From 82090cb7803fb21f7e3fb4d42710aed1f570ece7 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Tue, 7 Jul 2009 23:55:20 -0700 Subject: [PATCH 01/19] modified the timestamp parsing to run faster, making a big difference when loading huge datasets --- lib/couchrest/mixins/properties.rb | 40 +++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 6a69b44..5d3e8ed 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -1,6 +1,25 @@ require 'time' require File.join(File.dirname(__FILE__), '..', 'more', 'property') +class Time + # returns a local time value much faster than Time.parse + def self.mktime_with_offset(string) + string =~ /(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([\+\-])(\d{2})/ + # $1 = year + # $2 = month + # $3 = day + # $4 = hours + # $5 = minutes + # $6 = seconds + # $7 = time zone direction + # $8 = tz difference + # utc time with wrong TZ info: + time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7) + tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600) + time + tz_difference + zone_offset(time.zone) + end +end + module CouchRest module Mixins module Properties @@ -24,7 +43,7 @@ module CouchRest self.class.properties.each do |property| key = property.name.to_s # let's make sure we have a default - unless property.default.nil? + if property.default if property.default.class == Proc self[key] = property.default.call else @@ -37,10 +56,11 @@ module CouchRest def cast_keys return unless self.class.properties self.class.properties.each do |property| + next unless property.casted key = self.has_key?(property.name) ? property.name : property.name.to_sym # Don't cast the property unless it has a value - next unless self[key] + next unless self[key] target = property.type if target.is_a?(Array) klass = ::CouchRest.constantize(target[0]) @@ -48,19 +68,21 @@ module CouchRest # Auto parse Time objects obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value) obj.casted_by = self if obj.respond_to?(:casted_by) - obj + obj end else # Auto parse Time objects - self[property.name] = if ((property.init_method == 'new') && target == 'Time') - self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key] + self[property.name] = if ((property.init_method == 'new') && target == 'Time') + # Using custom time parsing method because Ruby's default method is toooo slow + self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key] else # Let people use :send as a Time parse arg klass = ::CouchRest.constantize(target) - klass.send(property.init_method, self[key].dup) - end + klass.send(property.init_method, self[key].dup) + end self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by) - end + end + end end @@ -122,4 +144,4 @@ module CouchRest end end -end +end \ No newline at end of file From baabe406747e60d993c8d7b515db49dec8c1ea68 Mon Sep 17 00:00:00 2001 From: Rob Kaufman Date: Tue, 7 Jul 2009 17:20:53 -0700 Subject: [PATCH 02/19] Fixed validates_is_numeric when dealing with an actual float --- lib/couchrest/validation/validators/numeric_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchrest/validation/validators/numeric_validator.rb b/lib/couchrest/validation/validators/numeric_validator.rb index 57a711f..05269b3 100644 --- a/lib/couchrest/validation/validators/numeric_validator.rb +++ b/lib/couchrest/validation/validators/numeric_validator.rb @@ -40,7 +40,7 @@ module CouchRest value = target.send(field_name) return true if @options[:allow_nil] && value.nil? - value = value.kind_of?(Float) ? value.to_s('F') : value.to_s + value = (defined?(BigDecimal) && value.kind_of?(BigDecimal)) ? value.to_s('F') : value.to_s error_message = @options[:message] precision = @options[:precision] From 9a89db44f172061485dd01d724b93b0e266b594b Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 8 Jul 2009 09:28:15 -0700 Subject: [PATCH 03/19] fixed a commit that got reverted by accident --- lib/couchrest/mixins/properties.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 5d3e8ed..19479e3 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -43,7 +43,7 @@ module CouchRest self.class.properties.each do |property| key = property.name.to_s # let's make sure we have a default - if property.default + unless property.default.nil? if property.default.class == Proc self[key] = property.default.call else From 3e2b3ece4637b4c34b904b71db4bbadad7762616 Mon Sep 17 00:00:00 2001 From: Seth Falcon Date: Fri, 12 Jun 2009 14:06:07 -0700 Subject: [PATCH 04/19] Timeout::TimeoutError does not exist, use Timeout::Error instead Also added a require for 'timeout' that contains this code. Easy to get confused as there is an alias TimeoutError: irb(main):001:0> require 'timeout' => true irb(main):002:0> TimeoutError => Timeout::Error irb(main):003:0> Timeout::Error => Timeout::Error irb(main):004:0> Timeout::TimeoutError NameError: uninitialized constant Timeout::TimeoutError from (irb):4 --- lib/couchrest/monkeypatches.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/monkeypatches.rb b/lib/couchrest/monkeypatches.rb index 2fad1f3..bebd49a 100644 --- a/lib/couchrest/monkeypatches.rb +++ b/lib/couchrest/monkeypatches.rb @@ -1,5 +1,6 @@ require File.join(File.dirname(__FILE__), 'support', 'class') require File.join(File.dirname(__FILE__), 'support', 'blank') +require 'timeout' # This file must be loaded after the JSON gem and any other library that beats up the Time class. class Time @@ -38,7 +39,7 @@ if RUBY_VERSION.to_f < 1.9 if IO.select([@io], nil, nil, @read_timeout) retry else - raise Timeout::TimeoutError + raise Timeout::Error end end else @@ -109,4 +110,4 @@ module RestClient # end # end -end \ No newline at end of file +end From cf764667953d1bc75896ae2001832681fefed61f Mon Sep 17 00:00:00 2001 From: John Wood Date: Thu, 4 Jun 2009 13:43:14 -0500 Subject: [PATCH 05/19] Added Collection mixin. The Collection mixin adds support for executing a view, and passing back the view results as an Array of the given ExtendedDocument instance. It also supports will_paginate like pagination methods (paginate, paginated_each), which will only fetch the given set of documents from CouchDB. --- couchrest.gemspec | 2 +- lib/couchrest/mixins/collection.rb | 153 ++++++++++++++++++ .../mixins/extended_document_mixins.rb | 1 + lib/couchrest/more/extended_document.rb | 7 +- 4 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 lib/couchrest/mixins/collection.rb diff --git a/couchrest.gemspec b/couchrest.gemspec index 13b4be7..95997b4 100644 --- a/couchrest.gemspec +++ b/couchrest.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |s| s.description = %q{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.} s.email = %q{jchris@apache.org} s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] - s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/helper/upgrade.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/class_proxy.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/support/rails.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_extended_doc_spec.rb", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_subclass_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/cat.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] + s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/helper/upgrade.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/class_proxy.rb", "lib/couchrest/mixins/collection.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/support/rails.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_extended_doc_spec.rb", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_subclass_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/cat.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] s.has_rdoc = true s.homepage = %q{http://github.com/jchris/couchrest} s.require_paths = ["lib"] diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb new file mode 100644 index 0000000..3dac41b --- /dev/null +++ b/lib/couchrest/mixins/collection.rb @@ -0,0 +1,153 @@ +module CouchRest + module Mixins + module Collection + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def provides_collection(collection_name, collection_options) + class_eval <<-END, __FILE__, __LINE__ + 1 + def self.find_all_#{collection_name}(options = {}) + view_name = "#{collection_options[:through][:view_name]}" + view_options = #{collection_options[:through][:view_options].inspect} || {} + CollectionProxy.new(@database, view_name, view_options.merge(options), Kernel.const_get('#{self}')) + end + END + end + + def paginate(options) + proxy = create_collection_proxy(options) + proxy.paginate(options) + end + + def paginated_each(options, &block) + proxy = create_collection_proxy(options) + proxy.paginated_each(options, &block) + end + + private + + def create_collection_proxy(options) + view_name, view_options = parse_view_options(options) + CollectionProxy.new(@database, view_name, view_options, self) + end + + def parse_view_options(options) + raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys + options = options.symbolize_keys + + view_name = options.delete(:view_name) + raise ArgumentError, 'view_name is required' if view_name.nil? + + view_options = options.delete(:view_options) || {} + + [view_name, view_options] + end + end + + class CollectionProxy + alias_method :proxy_respond_to?, :respond_to? + instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ } + + def initialize(database, view_name, view_options = {}, container_class = nil) + @container_class = container_class + @database = database + @view_name = view_name + @view_options = view_options + end + + def paginate(options = {}) + page, per_page = parse_options(options) + rows = @database.view(@view_name, @view_options.merge(pagination_options(page, per_page)))['rows'] + convert_to_container_array(rows) + end + + def paginated_each(options = {}, &block) + page, per_page = parse_options(options) + + begin + collection = paginate({:page => page, :per_page => per_page}) + collection.each(&block) + page += 1 + end until collection.size < per_page + end + + def respond_to?(*args) + proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) + end + + # Explicitly proxy === because the instance method removal above + # doesn't catch it. + def ===(other) + load_target + other === @target + end + + private + + def method_missing(method, *args) + if load_target + if block_given? + @target.send(method, *args) { |*block_args| yield(*block_args) } + else + @target.send(method, *args) + end + end + end + + def load_target + unless loaded? + rows = @database.view(@view_name, @view_options)['rows'] + @target = convert_to_container_array(rows) + end + @loaded = true + @target + end + + def loaded? + @loaded + end + + def reload + reset + load_target + self unless @target.nil? + end + + def reset + @loaded = false + @target = nil + end + + def inspect + load_target + @target.inspect + end + + def convert_to_container_array(rows) + return rows if @container_class.nil? + + container = [] + rows.each { |row| container << @container_class.new(row['value']) } unless rows.nil? + container + end + + def pagination_options(page, per_page) + { :limit => per_page, :skip => per_page * (page - 1) } + end + + def parse_options(options) + raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys + options = options.symbolize_keys + + page = options[:page] || 1 + per_page = options[:per_page] || 30 + [page, per_page] + 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 index f5aa8f9..89b25d6 100644 --- a/lib/couchrest/mixins/extended_document_mixins.rb +++ b/lib/couchrest/mixins/extended_document_mixins.rb @@ -5,3 +5,4 @@ require File.join(File.dirname(__FILE__), 'design_doc') require File.join(File.dirname(__FILE__), 'validation') require File.join(File.dirname(__FILE__), 'extended_attachments') require File.join(File.dirname(__FILE__), 'class_proxy') +require File.join(File.dirname(__FILE__), 'collection') diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 98a402f..14006ed 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -13,10 +13,11 @@ module CouchRest include CouchRest::Mixins::DesignDoc include CouchRest::Mixins::ExtendedAttachments include CouchRest::Mixins::ClassProxy + include CouchRest::Mixins::Collection - def self.subclasses - @subclasses ||= [] - end + def self.subclasses + @subclasses ||= [] + end def self.inherited(subklass) subklass.send(:include, CouchRest::Mixins::Properties) From 5963f1d4f85b606a9e95a1885da4179956b65ac3 Mon Sep 17 00:00:00 2001 From: John Wood Date: Thu, 18 Jun 2009 11:28:57 -0500 Subject: [PATCH 06/19] Better integration with couchrest views. More tests, doc, and some cleanup still needed. --- lib/couchrest/mixins/collection.rb | 55 +++++++++++-------- lib/couchrest/mixins/views.rb | 8 ++- spec/couchrest/more/extended_doc_view_spec.rb | 34 +++++++++++- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index 3dac41b..f590ef3 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -27,23 +27,30 @@ module CouchRest proxy.paginated_each(options, &block) end + def collection_proxy_for(design_doc, view_name, view_options = {}) + options = view_options.merge(:design_doc => design_doc, :view_name => view_name) + create_collection_proxy(options) + end + private def create_collection_proxy(options) - view_name, view_options = parse_view_options(options) - CollectionProxy.new(@database, view_name, view_options, self) + view_name, view_options, design_doc = parse_view_options(options) + CollectionProxy.new(@database, design_doc, view_name, view_options, self) end def parse_view_options(options) - raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys - options = options.symbolize_keys + design_doc = options.delete(:design_doc) + raise ArgumentError, 'design_doc is required' if design_doc.nil? view_name = options.delete(:view_name) raise ArgumentError, 'view_name is required' if view_name.nil? view_options = options.delete(:view_options) || {} + default_view_options = (design_doc['views'][view_name.to_s] && design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {} + view_options = default_view_options.merge(view_options).merge(options) - [view_name, view_options] + [view_name, view_options, design_doc] end end @@ -51,17 +58,24 @@ module CouchRest alias_method :proxy_respond_to?, :respond_to? instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ } - def initialize(database, view_name, view_options = {}, container_class = nil) - @container_class = container_class + def initialize(database, design_doc, view_name, view_options = {}, container_class = nil) + raise ArgumentError, "database is a required parameter" if database.nil? + @database = database - @view_name = view_name @view_options = view_options + @container_class = container_class + + if design_doc.class == Design + @view_name = "#{design_doc.name}/#{view_name}" + else + @view_name = "#{design_doc}/#{view_name}" + end end def paginate(options = {}) page, per_page = parse_options(options) - rows = @database.view(@view_name, @view_options.merge(pagination_options(page, per_page)))['rows'] - convert_to_container_array(rows) + results = @database.view(@view_name, @view_options.merge(pagination_options(page, per_page))) + convert_to_container_array(results) end def paginated_each(options = {}, &block) @@ -99,8 +113,8 @@ module CouchRest def load_target unless loaded? - rows = @database.view(@view_name, @view_options)['rows'] - @target = convert_to_container_array(rows) + results = @database.view(@view_name, @view_options) + @target = convert_to_container_array(results) end @loaded = true @target @@ -126,12 +140,12 @@ module CouchRest @target.inspect end - def convert_to_container_array(rows) - return rows if @container_class.nil? - - container = [] - rows.each { |row| container << @container_class.new(row['value']) } unless rows.nil? - container + def convert_to_container_array(results) + if @container_class.nil? + results + else + results['rows'].collect { |row| @container_class.new(row['doc']) } unless results['rows'].nil? + end end def pagination_options(page, per_page) @@ -139,12 +153,9 @@ module CouchRest end def parse_options(options) - raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys - options = options.symbolize_keys - page = options[:page] || 1 per_page = options[:per_page] || 30 - [page, per_page] + [page.to_i, per_page.to_i] end end diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/mixins/views.rb index 55c1613..420b87a 100644 --- a/lib/couchrest/mixins/views.rb +++ b/lib/couchrest/mixins/views.rb @@ -141,8 +141,12 @@ module CouchRest fetch_view(db, name, opts, &block) else begin - view = fetch_view db, name, opts.merge({:include_docs => true}), &block - view['rows'].collect{|r|new(r['doc'])} if view['rows'] + if block.nil? + collection_proxy_for(design_doc, name, opts.merge({:include_docs => true})) + else + view = fetch_view db, name, opts.merge({:include_docs => true}), &block + view['rows'].collect{|r|new(r['doc'])} if view['rows'] + end rescue # fallback for old versions of couchdb that don't # have include_docs support diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb index 4d797e5..bfaa2c4 100644 --- a/spec/couchrest/more/extended_doc_view_spec.rb +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -337,5 +337,37 @@ describe "ExtendedDocument views" do Article.design_doc["views"].keys.should include("by_updated_at") end end - + + describe "with a collection" do + before(:all) do + reset_test_db! + @titles = ["very uniq one", "really interesting", "some fun", + "really awesome", "crazy bob", "this rocks", "super rad"] + @titles.each_with_index do |title,i| + a = Article.new(:title => title, :date => Date.today) + a.save + end + end + it "should return an array of 7 Article objects" do + articles = Article.by_date :key => Date.today + articles.class.should == Array + articles.size.should == 7 + end + it "should get a subset of articles using paginate" do + articles = Article.by_date :key => Date.today + articles.paginate(:page => 1, :per_page => 3).size.should == 3 + articles.paginate(:page => 2, :per_page => 3).size.should == 3 + articles.paginate(:page => 3, :per_page => 3).size.should == 1 + end +# it "should get all articles, a few at a time, using paginated each" do +# +# end +# it "should provide a class method to access the collection directly" do +# +# end +# it "should provide class methods for pagination" do +# +# end + end + end From a9a53b872944a3582eba167300948e4714412304 Mon Sep 17 00:00:00 2001 From: John Wood Date: Fri, 19 Jun 2009 11:00:52 -0500 Subject: [PATCH 07/19] Added more tests for Collection module, cleaned up the code as well. --- lib/couchrest/mixins/collection.rb | 43 +++++++++---- spec/couchrest/more/extended_doc_view_spec.rb | 63 ++++++++++++++++--- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index f590ef3..ab0c7eb 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -7,12 +7,23 @@ module CouchRest end module ClassMethods + + # Creates a new class method, find_all_ on the class + # that will execute the view specified in the collection_options, + # along with all view options specified. This method will return the + # results of the view as an Array of objects which are instances of the + # class. + # + # This method is handy for objects that do not use the view_by method + # to declare their views. + # def provides_collection(collection_name, collection_options) class_eval <<-END, __FILE__, __LINE__ + 1 def self.find_all_#{collection_name}(options = {}) - view_name = "#{collection_options[:through][:view_name]}" - view_options = #{collection_options[:through][:view_options].inspect} || {} - CollectionProxy.new(@database, view_name, view_options.merge(options), Kernel.const_get('#{self}')) + design_doc = "#{collection_options[:through].delete(:design_doc)}" + view_name = "#{collection_options[:through].delete(:view_name)}" + view_options = #{collection_options[:through].inspect} || {} + CollectionProxy.new(@database, design_doc, view_name, view_options.merge(options), Kernel.const_get('#{self}')) end END end @@ -35,7 +46,7 @@ module CouchRest private def create_collection_proxy(options) - view_name, view_options, design_doc = parse_view_options(options) + design_doc, view_name, view_options = parse_view_options(options) CollectionProxy.new(@database, design_doc, view_name, view_options, self) end @@ -46,11 +57,12 @@ module CouchRest view_name = options.delete(:view_name) raise ArgumentError, 'view_name is required' if view_name.nil? - view_options = options.delete(:view_options) || {} - default_view_options = (design_doc['views'][view_name.to_s] && design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {} - view_options = default_view_options.merge(view_options).merge(options) + default_view_options = (design_doc.class == Design && + design_doc['views'][view_name.to_s] && + design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {} + view_options = default_view_options.merge(options) - [view_name, view_options, design_doc] + [design_doc, view_name, view_options] end end @@ -58,13 +70,18 @@ module CouchRest alias_method :proxy_respond_to?, :respond_to? instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ } + DEFAULT_PAGE = 1 + DEFAULT_PER_PAGE = 30 + def initialize(database, design_doc, view_name, view_options = {}, container_class = nil) raise ArgumentError, "database is a required parameter" if database.nil? @database = database - @view_options = view_options @container_class = container_class + strip_pagination_options(view_options) + @view_options = view_options + if design_doc.class == Design @view_name = "#{design_doc.name}/#{view_name}" else @@ -153,10 +170,14 @@ module CouchRest end def parse_options(options) - page = options[:page] || 1 - per_page = options[:per_page] || 30 + page = options.delete(:page) || DEFAULT_PAGE + per_page = options.delete(:per_page) || DEFAULT_PER_PAGE [page.to_i, per_page.to_i] end + + def strip_pagination_options(options) + parse_options(options) + end end end diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb index bfaa2c4..1bdc58d 100644 --- a/spec/couchrest/more/extended_doc_view_spec.rb +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -348,7 +348,7 @@ describe "ExtendedDocument views" do a.save end end - it "should return an array of 7 Article objects" do + it "should return a proxy that looks like an array of 7 Article objects" do articles = Article.by_date :key => Date.today articles.class.should == Array articles.size.should == 7 @@ -359,15 +359,58 @@ describe "ExtendedDocument views" do articles.paginate(:page => 2, :per_page => 3).size.should == 3 articles.paginate(:page => 3, :per_page => 3).size.should == 1 end -# it "should get all articles, a few at a time, using paginated each" do -# -# end -# it "should provide a class method to access the collection directly" do -# -# end -# it "should provide class methods for pagination" do -# -# end + it "should get all articles, a few at a time, using paginated each" do + articles = Article.by_date :key => Date.today + articles.paginated_each(:per_page => 3) do |a| + a.should_not be_nil + end + end + it "should provide a class method to access the collection directly" do + articles = Article.collection_proxy_for('Article', 'by_date', :descending => true, + :key => Date.today, :include_docs => true) + articles.class.should == Array + articles.size.should == 7 + end + it "should provide a class method for paginate" do + articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', + :per_page => 3, :descending => true, :key => Date.today, :include_docs => true) + articles.size.should == 3 + + articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', + :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true) + articles.size.should == 3 + + articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date', + :per_page => 3, :page => 3, :descending => true, :key => Date.today, :include_docs => true) + articles.size.should == 1 + end + it "should provide a class method for paginated_each" do + options = { :design_doc => 'Article', :view_name => 'by_date', + :per_page => 3, :page => 1, :descending => true, :key => Date.today, + :include_docs => true } + Article.paginated_each(options) do |a| + a.should_not be_nil + end + end + it "should provide a class method to get a collection for a view" do + class Article + provides_collection :article_details, :through => { + :design_doc => 'Article', :view_name => 'by_date', :descending => true, + :include_docs => true } + end + + articles = Article.find_all_article_details(:key => Date.today) + articles.class.should == Array + articles.size.should == 7 + end + it "should raise an exception if design_doc is not provided" do + lambda{Article.collection_proxy_for(nil, 'by_date')}.should raise_error + lambda{Article.paginate(:view_name => 'by_date')}.should raise_error + end + it "should raise an exception if view_name is not provided" do + lambda{Article.collection_proxy_for('Article', nil)}.should raise_error + lambda{Article.paginate(:design_doc => 'Article')}.should raise_error + end end end From a0d6204b423c3c65dc783495db81c79b71a2d6bf Mon Sep 17 00:00:00 2001 From: John Wood Date: Fri, 19 Jun 2009 11:17:40 -0500 Subject: [PATCH 08/19] Added some more doc for Collection, and cleaned up how provides_collection works. --- lib/couchrest/mixins/collection.rb | 42 ++++++++++++++----- spec/couchrest/more/extended_doc_view_spec.rb | 4 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index ab0c7eb..058746b 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -8,36 +8,47 @@ module CouchRest module ClassMethods - # Creates a new class method, find_all_ on the class - # that will execute the view specified in the collection_options, - # along with all view options specified. This method will return the - # results of the view as an Array of objects which are instances of the - # class. + # Creates a new class method, find_all_, that will + # execute the view specified with the design_doc and view_name + # parameters, along with the specified view_options. This method will + # return the results of the view as an Array of objects which are + # instances of the class. # # This method is handy for objects that do not use the view_by method # to declare their views. - # - def provides_collection(collection_name, collection_options) + def provides_collection(collection_name, design_doc, view_name, view_options) class_eval <<-END, __FILE__, __LINE__ + 1 def self.find_all_#{collection_name}(options = {}) - design_doc = "#{collection_options[:through].delete(:design_doc)}" - view_name = "#{collection_options[:through].delete(:view_name)}" - view_options = #{collection_options[:through].inspect} || {} - CollectionProxy.new(@database, design_doc, view_name, view_options.merge(options), Kernel.const_get('#{self}')) + view_options = #{view_options.inspect} || {} + CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}')) end END end + # Fetch a group of objects from CouchDB. Options can include: + # :page - Specifies the page to load (starting at 1) + # :per_page - Specifies the number of objects to load per page + # + # Defaults are used if these options are not specified. def paginate(options) proxy = create_collection_proxy(options) proxy.paginate(options) end + # Iterate over the objects in a collection, fetching them from CouchDB + # in groups. Options can include: + # :page - Specifies the page to load + # :per_page - Specifies the number of objects to load per page + # + # Defaults are used if these options are not specified. def paginated_each(options, &block) proxy = create_collection_proxy(options) proxy.paginated_each(options, &block) end + # Create a CollectionProxy for the specified view and options. + # CollectionProxy behaves just like an Array, but offers support for + # pagination. def collection_proxy_for(design_doc, view_name, view_options = {}) options = view_options.merge(:design_doc => design_doc, :view_name => view_name) create_collection_proxy(options) @@ -73,6 +84,13 @@ module CouchRest DEFAULT_PAGE = 1 DEFAULT_PER_PAGE = 30 + # Create a new CollectionProxy to represent the specified view. If a + # container class is specified, the proxy will create an object of the + # given type for each row that comes back from the view. If no + # container class is specified, the raw results are returned. + # + # 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) raise ArgumentError, "database is a required parameter" if database.nil? @@ -89,12 +107,14 @@ module CouchRest end end + # See Collection.paginate def paginate(options = {}) page, per_page = parse_options(options) results = @database.view(@view_name, @view_options.merge(pagination_options(page, per_page))) convert_to_container_array(results) end + # See Collection.paginated_each def paginated_each(options = {}, &block) page, per_page = parse_options(options) diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb index 1bdc58d..f7c5361 100644 --- a/spec/couchrest/more/extended_doc_view_spec.rb +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -394,9 +394,7 @@ describe "ExtendedDocument views" do end it "should provide a class method to get a collection for a view" do class Article - provides_collection :article_details, :through => { - :design_doc => 'Article', :view_name => 'by_date', :descending => true, - :include_docs => true } + provides_collection :article_details, 'Article', 'by_date', :descending => true, :include_docs => true end articles = Article.find_all_article_details(:key => Date.today) From 42482a626a20c6224bdc4f3eb3143664ee99e59a Mon Sep 17 00:00:00 2001 From: John Wood Date: Mon, 22 Jun 2009 08:39:28 -0500 Subject: [PATCH 09/19] Changed pagination technique used by Collection Modified Collection to use the pagination technique described at http://wiki.apache.org/couchdb/How_to_page_through_results where possible. --- lib/couchrest/mixins/collection.rb | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index 058746b..3af8d74 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -110,7 +110,8 @@ module CouchRest # See Collection.paginate def paginate(options = {}) page, per_page = parse_options(options) - results = @database.view(@view_name, @view_options.merge(pagination_options(page, per_page))) + results = @database.view(@view_name, pagination_options(page, per_page)) + remember_where_we_left_off(results, page) convert_to_container_array(results) end @@ -186,7 +187,14 @@ module CouchRest end def pagination_options(page, per_page) - { :limit => per_page, :skip => per_page * (page - 1) } + view_options = @view_options.clone + if @last_key && @last_docid && @last_page == page - 1 + view_options.delete(:key) + options = { :startkey => @last_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 } + else + options = { :limit => per_page, :skip => per_page * (page - 1) } + end + view_options.merge(options) end def parse_options(options) @@ -198,6 +206,13 @@ module CouchRest def strip_pagination_options(options) parse_options(options) end + + def remember_where_we_left_off(results, page) + last_row = results['rows'].last + @last_key = last_row['key'] + @last_docid = last_row['id'] + @last_page = page + end end end From bd1b1149309019703d75f61d5f2b0ad553ec053d Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Wed, 8 Jul 2009 11:54:06 -0700 Subject: [PATCH 10/19] bumped version to 0.30 and added history.txt + pagination doc in the readme --- README.md | 22 ++++++++++++++++++++++ Rakefile | 2 +- couchrest.gemspec | 7 +++---- history.txt | 19 +++++++++++++++++++ lib/couchrest.rb | 2 +- 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 history.txt diff --git a/README.md b/README.md index 2e6f9bf..4ef2748 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,25 @@ you can define some casting rules. If you want to cast an array of instances from a specific Class, use the trick shown above ["ClassName"] +### Pagination + +Pagination is available in any ExtendedDocument classes. Here are some usage examples: + +basic usage: + + Article.all.paginate(:page => 1, :per_page => 5) + +note: the above query will look like: `GET /db/_design/Article/_view/all?include_docs=true&skip=0&limit=5&reduce=false` and only fetch 5 documents. + +Slightly more advance usage: + + Article.by_name(:startkey => 'a', :endkey => {}).paginate(:page => 1, :per_page => 5) + +note: the above query will look like: `GET /db/_design/Article/_view/by_name?startkey=%22a%22&limit=5&skip=0&endkey=%7B%7D&include_docs=true` +Basically, you can paginate through the articles starting by the letter a, 5 articles at a time. + + +Low level usage: + + Article.paginate(:design_doc => 'Article', :view_name => 'by_date', + :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true) \ No newline at end of file diff --git a/Rakefile b/Rakefile index ddea44d..90b0653 100644 --- a/Rakefile +++ b/Rakefile @@ -24,7 +24,7 @@ spec = Gem::Specification.new do |s| s.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." s.has_rdoc = true s.authors = ["J. Chris Anderson", "Matt Aimonetti"] - s.files = %w( LICENSE README.md Rakefile THANKS.md ) + + s.files = %w( LICENSE README.md Rakefile THANKS.md history.txt) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"] s.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) diff --git a/couchrest.gemspec b/couchrest.gemspec index 95997b4..8708ec6 100644 --- a/couchrest.gemspec +++ b/couchrest.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = %q{couchrest} - s.version = "0.29" + s.version = "0.30" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["J. Chris Anderson", "Matt Aimonetti"] @@ -10,11 +10,10 @@ Gem::Specification.new do |s| s.description = %q{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.} s.email = %q{jchris@apache.org} s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] - s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/helper/upgrade.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/class_proxy.rb", "lib/couchrest/mixins/collection.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/support/rails.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_extended_doc_spec.rb", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_subclass_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/cat.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] - s.has_rdoc = true + s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "history.txt", "examples/model", "examples/model/example.rb", "examples/word_count", "examples/word_count/markov", "examples/word_count/views", "examples/word_count/views/books", "examples/word_count/views/books/chunked-map.js", "examples/word_count/views/books/united-map.js", "examples/word_count/views/markov", "examples/word_count/views/markov/chain-map.js", "examples/word_count/views/markov/chain-reduce.js", "examples/word_count/views/word_count", "examples/word_count/views/word_count/count-map.js", "examples/word_count/views/word_count/count-reduce.js", "examples/word_count/word_count.rb", "examples/word_count/word_count_query.rb", "examples/word_count/word_count_views.rb", "lib/couchrest", "lib/couchrest/commands", "lib/couchrest/commands/generate.rb", "lib/couchrest/commands/push.rb", "lib/couchrest/core", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/helper/upgrade.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/class_proxy.rb", "lib/couchrest/mixins/collection.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/support/rails.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_extended_doc_spec.rb", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_subclass_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/cat.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] s.homepage = %q{http://github.com/jchris/couchrest} s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.2} + s.rubygems_version = %q{1.3.4} s.summary = %q{Lean and RESTful interface to CouchDB.} if s.respond_to? :specification_version then diff --git a/history.txt b/history.txt new file mode 100644 index 0000000..a8db62f --- /dev/null +++ b/history.txt @@ -0,0 +1,19 @@ +== 0.30 + +* Major enhancements + + * Added support for pagination (John Wood) + * Improved performance when initializing documents with timestamps (Matt Aimonetti) + +* Minor enhancements + + * Extended the API to retrieve an attachment URI (Matt Aimonetti) + * Bug fix: default value should be able to be set as false (Alexander Uvarov) + * Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman) + * Bug fix: fixed the Timeout implementation (Seth Falcon) + + +--- + +Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest. +You can see the full commit history on GitHub: http://github.com/mattetti/couchrest/commits/master/ \ No newline at end of file diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 75459d1..1bad3ad 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -28,7 +28,7 @@ require 'couchrest/monkeypatches' # = CouchDB, close to the metal module CouchRest - VERSION = '0.29' unless self.const_defined?("VERSION") + VERSION = '0.30' unless self.const_defined?("VERSION") autoload :Server, 'couchrest/core/server' autoload :Database, 'couchrest/core/database' From b2a29d9eb795dc50c02ee361bb908aa04c3b5514 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Tue, 14 Jul 2009 01:43:40 -0700 Subject: [PATCH 11/19] started extracting the http layer --- lib/couchrest.rb | 15 +-- lib/couchrest/core/adapters/restclient.rb | 35 +++++++ lib/couchrest/core/database.rb | 15 ++- lib/couchrest/core/document.rb | 4 - lib/couchrest/core/http_abstraction.rb | 48 +++++++++ lib/couchrest/mixins/views.rb | 2 +- lib/couchrest/monkeypatches.rb | 118 +++++++++++----------- spec/couchrest/core/couchrest_spec.rb | 2 +- 8 files changed, 158 insertions(+), 81 deletions(-) create mode 100644 lib/couchrest/core/adapters/restclient.rb create mode 100644 lib/couchrest/core/http_abstraction.rb diff --git a/lib/couchrest.rb b/lib/couchrest.rb index 1bad3ad..e824563 100644 --- a/lib/couchrest.rb +++ b/lib/couchrest.rb @@ -45,6 +45,7 @@ module CouchRest autoload :ExtendedDocument, 'couchrest/more/extended_document' autoload :CastedModel, 'couchrest/more/casted_model' + require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction') require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') # The CouchRest module methods handle the basic JSON serialization @@ -118,9 +119,9 @@ module CouchRest } end - # set proxy for RestClient to use + # set proxy to use def proxy url - RestClient.proxy = url + HttpAbstraction.proxy = url end # ensure that a database exists @@ -141,7 +142,7 @@ module CouchRest def put(uri, doc = nil) payload = doc.to_json if doc begin - JSON.parse(RestClient.put(uri, payload)) + JSON.parse(HttpAbstraction.put(uri, payload)) rescue Exception => e if $DEBUG raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}" @@ -153,7 +154,7 @@ module CouchRest def get(uri) begin - JSON.parse(RestClient.get(uri), :max_nesting => false) + JSON.parse(HttpAbstraction.get(uri), :max_nesting => false) rescue => e if $DEBUG raise "Error while sending a GET request #{uri}\n: #{e}" @@ -166,7 +167,7 @@ module CouchRest def post uri, doc = nil payload = doc.to_json if doc begin - JSON.parse(RestClient.post(uri, payload)) + JSON.parse(HttpAbstraction.post(uri, payload)) rescue Exception => e if $DEBUG raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}" @@ -177,11 +178,11 @@ module CouchRest end def delete uri - JSON.parse(RestClient.delete(uri)) + JSON.parse(HttpAbstraction.delete(uri)) end def copy uri, destination - JSON.parse(RestClient.copy(uri, {'Destination' => destination})) + JSON.parse(HttpAbstraction.copy(uri, {'Destination' => destination})) end def paramify_url url, params = {} diff --git a/lib/couchrest/core/adapters/restclient.rb b/lib/couchrest/core/adapters/restclient.rb new file mode 100644 index 0000000..ed02228 --- /dev/null +++ b/lib/couchrest/core/adapters/restclient.rb @@ -0,0 +1,35 @@ +module RestClientAdapter + + module API + def proxy=(url) + RestClient.proxy = url + end + + def proxy + RestClient.proxy + end + + def get(uri, headers={}) + RestClient.get(uri, headers) + end + + def post(uri, payload, headers={}) + RestClient.post(uri, payload, headers) + end + + def put(uri, payload, headers={}) + RestClient.put(uri, payload, headers) + end + + def delete(uri, headers={}) + RestClient.delete(uri, headers) + end + + def copy(uri, headers) + RestClient::Request.execute( :method => :copy, + :url => uri, + :headers => headers) + end + end + +end \ No newline at end of file diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 6627117..08eb4cb 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -58,7 +58,7 @@ module CouchRest keys = params.delete(:keys) funcs = funcs.merge({:keys => keys}) if keys url = CouchRest.paramify_url "#{@root}/_temp_view", params - JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'})) + JSON.parse(HttpAbstraction.post(url, funcs.to_json, {"Content-Type" => 'application/json'})) end # backwards compatibility is a plus @@ -100,11 +100,8 @@ module CouchRest # GET an attachment directly from CouchDB def fetch_attachment(doc, name) - # slug = escape_docid(docid) - # name = CGI.escape(name) uri = url_for_attachment(doc, name) - RestClient.get uri - # "#{@uri}/#{slug}/#{name}" + HttpAbstraction.get uri end # PUT an attachment directly to CouchDB @@ -112,14 +109,14 @@ module CouchRest docid = escape_docid(doc['_id']) name = CGI.escape(name) uri = url_for_attachment(doc, name) - JSON.parse(RestClient.put(uri, file, options)) + JSON.parse(HttpAbstraction.put(uri, file, options)) end # DELETE an attachment directly from CouchDB def delete_attachment doc, name uri = url_for_attachment(doc, name) # this needs a rev - JSON.parse(RestClient.delete(uri)) + JSON.parse(HttpAbstraction.delete(uri)) end # Save a document to CouchDB. This will use the _id field from @@ -146,7 +143,7 @@ module CouchRest slug = escape_docid(doc['_id']) begin CouchRest.put "#{@root}/#{slug}", doc - rescue RestClient::ResourceNotFound + rescue HttpAbstraction::ResourceNotFound p "resource not found when saving even tho an id was passed" slug = doc['_id'] = @server.next_uuid CouchRest.put "#{@root}/#{slug}", doc @@ -252,7 +249,7 @@ module CouchRest def recreate! delete! create! - rescue RestClient::ResourceNotFound + rescue HttpAbstraction::ResourceNotFound ensure create! end diff --git a/lib/couchrest/core/document.rb b/lib/couchrest/core/document.rb index 941ed80..44c4f5a 100644 --- a/lib/couchrest/core/document.rb +++ b/lib/couchrest/core/document.rb @@ -3,10 +3,6 @@ require 'delegate' module CouchRest class Document < Response include CouchRest::Mixins::Attachments - - # def self.inherited(subklass) - # subklass.send(:extlib_inheritable_accessor, :database) - # end extlib_inheritable_accessor :database attr_accessor :database diff --git a/lib/couchrest/core/http_abstraction.rb b/lib/couchrest/core/http_abstraction.rb new file mode 100644 index 0000000..529866d --- /dev/null +++ b/lib/couchrest/core/http_abstraction.rb @@ -0,0 +1,48 @@ +require 'couchrest/core/adapters/restclient' + +# Abstraction layet for HTTP communications. +# +# By defining a basic API that CouchRest is relying on, +# it allows for easy experimentations and implementations of various libraries. +# +# Most of the API is based on the RestClient API that was used in the early version of CouchRest. +# +module HttpAbstraction + + # here is the list of exception expected by CouchRest + # please convert the underlying errors in this set of known + # exceptions. + class ResourceNotFound < StandardError; end + class RequestFailed < StandardError; end + class RequestTimeout < StandardError; end + class ServerBrokeConnection < StandardError; end + class Conflict < StandardError; end + + + # # Here is the API you need to implement if you want to write a new adapter + # # See adapters/restclient.rb for more information. + # + # def self.proxy=(url) + # end + # + # def self.proxy + # end + # + # def self.get(uri, headers=nil) + # end + # + # def self.post(uri, payload, headers=nil) + # end + # + # def self.put(uri, payload, headers=nil) + # end + # + # def self.delete(uri, headers=nil) + # end + # + # def self.copy(uri, headers) + # end + +end + +HttpAbstraction.extend(RestClientAdapter::API) \ No newline at end of file diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/mixins/views.rb index 420b87a..d6abeb0 100644 --- a/lib/couchrest/mixins/views.rb +++ b/lib/couchrest/mixins/views.rb @@ -162,7 +162,7 @@ module CouchRest begin design_doc.view_on(db, view_name, opts, &block) # the design doc may not have been saved yet on this database - rescue RestClient::ResourceNotFound => e + rescue HttpAbstraction::ResourceNotFound => e if retryable save_design_doc_on(db) retryable = false diff --git a/lib/couchrest/monkeypatches.rb b/lib/couchrest/monkeypatches.rb index bebd49a..95c52c3 100644 --- a/lib/couchrest/monkeypatches.rb +++ b/lib/couchrest/monkeypatches.rb @@ -51,63 +51,63 @@ if RUBY_VERSION.to_f < 1.9 end end -module RestClient - def self.copy(url, headers={}) - Request.execute(:method => :copy, - :url => url, - :headers => headers) - end - -# class Request -# -# def establish_connection(uri) -# Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?) -# p net_http_class -# net = net_http_class.new(uri.host, uri.port) -# net.use_ssl = uri.is_a?(URI::HTTPS) -# net.verify_mode = OpenSSL::SSL::VERIFY_NONE -# Thread.current[:connection] = net -# Thread.current[:connection].start -# Thread.current[:connection] -# end -# -# def transmit(uri, req, payload) -# setup_credentials(req) -# -# Thread.current[:host] ||= uri.host -# Thread.current[:port] ||= uri.port -# -# if (Thread.current[:connection].nil? || (Thread.current[:host] != uri.host)) -# p "establishing a connection" -# establish_connection(uri) -# end +# module RestClient +# # def self.copy(url, headers={}) +# # Request.execute(:method => :copy, +# # :url => url, +# # :headers => headers) +# # 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" -# establish_connection(uri) -# http = Thread.current[:connection] -# require 'ruby-debug' -# req.body_stream = nil -# -# res = http.request(req, payload) -# display_log response_log(res) -# 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 +# # class Request +# # +# # def establish_connection(uri) +# # Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?) +# # p net_http_class +# # net = net_http_class.new(uri.host, uri.port) +# # net.use_ssl = uri.is_a?(URI::HTTPS) +# # net.verify_mode = OpenSSL::SSL::VERIFY_NONE +# # Thread.current[:connection] = net +# # Thread.current[:connection].start +# # Thread.current[:connection] +# # end +# # +# # def transmit(uri, req, payload) +# # setup_credentials(req) +# # +# # Thread.current[:host] ||= uri.host +# # Thread.current[:port] ||= uri.port +# # +# # if (Thread.current[:connection].nil? || (Thread.current[:host] != uri.host)) +# # p "establishing a connection" +# # establish_connection(uri) +# # 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" +# # establish_connection(uri) +# # http = Thread.current[:connection] +# # require 'ruby-debug' +# # req.body_stream = nil +# # +# # res = http.request(req, payload) +# # display_log response_log(res) +# # 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 diff --git a/spec/couchrest/core/couchrest_spec.rb b/spec/couchrest/core/couchrest_spec.rb index 2ed8bb9..35e48e6 100644 --- a/spec/couchrest/core/couchrest_spec.rb +++ b/spec/couchrest/core/couchrest_spec.rb @@ -191,7 +191,7 @@ describe CouchRest do describe "using a proxy for RestClient connections" do it "should set proxy url for RestClient" do CouchRest.proxy 'http://localhost:8888/' - proxy_uri = URI.parse(RestClient.proxy) + proxy_uri = URI.parse(HttpAbstraction.proxy) proxy_uri.host.should eql( 'localhost' ) proxy_uri.port.should eql( 8888 ) CouchRest.proxy nil From 9a167cc27d3ee0a3407d73855e5dfed35139eb49 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Tue, 14 Jul 2009 23:48:06 -0700 Subject: [PATCH 12/19] fixed the specs --- lib/couchrest/mixins/views.rb | 10 +--------- spec/couchrest/core/database_spec.rb | 2 +- spec/couchrest/more/extended_doc_view_spec.rb | 9 +++++---- spec/spec_helper.rb | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/mixins/views.rb index d6abeb0..f520998 100644 --- a/lib/couchrest/mixins/views.rb +++ b/lib/couchrest/mixins/views.rb @@ -72,7 +72,7 @@ module CouchRest # # To understand the capabilities of this view system more completely, # it is recommended that you read the RSpec file at - # spec/core/model_spec.rb. + # spec/couchrest/more/extended_doc_spec.rb. def view_by(*keys) opts = keys.pop if keys.last.is_a?(Hash) @@ -124,14 +124,6 @@ module CouchRest # potentially large indexes. def cleanup_design_docs!(db = database) save_design_doc_on(db) - # db.refresh_design_doc - # db.save_design_doc - # design_doc = model_design_doc(db) - # if design_doc - # db.delete_doc(design_doc) - # else - # false - # end end private diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index 6df866d..3375779 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -690,7 +690,7 @@ describe CouchRest::Database do it "should recreate a db even tho it doesn't exist" do @cr.databases.should_not include(@db2.name) - @db2.recreate! + begin @db2.recreate! rescue nil end @cr.databases.should include(@db2.name) end diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb index f7c5361..7c5bbd2 100644 --- a/spec/couchrest/more/extended_doc_view_spec.rb +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -121,7 +121,7 @@ describe "ExtendedDocument views" do describe "a model class not tied to a database" do before(:all) do reset_test_db! - @db = DB + @db = DB %w{aaa bbb ddd eee}.each do |title| u = Unattached.new(:title => title) u.database = @db @@ -133,14 +133,15 @@ describe "ExtendedDocument views" do lambda{Unattached.all}.should raise_error end it "should query all" do - rs = Unattached.all :database=>@db + Unattached.cleanup_design_docs!(@db) + rs = Unattached.all :database => @db rs.length.should == 4 end it "should barf on query if no database given" do lambda{Unattached.view :by_title}.should raise_error end it "should make the design doc upon first query" do - Unattached.by_title :database=>@db + Unattached.by_title :database => @db doc = Unattached.design_doc doc['views']['all']['map'].should include('Unattached') end @@ -157,7 +158,7 @@ describe "ExtendedDocument views" do things = [] Unattached.view(:by_title, :database=>@db) do |thing| things << thing - end + end things[0]["doc"]["title"].should =='aaa' end it "should yield with by_key method" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cafdcc5..e750258 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -20,7 +20,7 @@ class Basic < CouchRest::ExtendedDocument end def reset_test_db! - DB.recreate! rescue nil + DB.recreate! rescue nil DB end From 8f8b5dc568a9886725053fd95bbb5ade2f6eab9b Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Thu, 16 Jul 2009 19:52:53 -0700 Subject: [PATCH 13/19] added support to cast Float values --- history.txt | 10 ++++++++++ lib/couchrest/mixins/properties.rb | 13 ++++++++++++- spec/couchrest/more/property_spec.rb | 23 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index a8db62f..0035981 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,13 @@ +== 0.31 + +* Major enhancements + + * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti) + +* Minor enhancements + + * Added Float casting (Ryan Felton & Matt Aimonetti) + == 0.30 * Major enhancements diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index 19479e3..2982d30 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -56,7 +56,6 @@ module CouchRest def cast_keys return unless self.class.properties self.class.properties.each do |property| - next unless property.casted key = self.has_key?(property.name) ? property.name : property.name.to_sym # Don't cast the property unless it has a value @@ -75,6 +74,9 @@ module CouchRest self[property.name] = if ((property.init_method == 'new') && target == 'Time') # Using custom time parsing method because Ruby's default method is toooo slow self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key] + # Float instances don't get initialized with #new + elsif ((property.init_method == 'new') && target == 'Float') + cast_float(self[key]) else # Let people use :send as a Time parse arg klass = ::CouchRest.constantize(target) @@ -84,6 +86,15 @@ module CouchRest end end + + def cast_float(value) + begin + Float(value) + rescue + value + end + end + end module ClassMethods diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 8559c8f..6782789 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -141,6 +141,29 @@ describe "ExtendedDocument properties" do @event['occurs_at'].should be_an_instance_of(Time) end end + + describe "casting to Float object" do + class RootBeerFloat < CouchRest::ExtendedDocument + use_database DB + property :price, :cast_as => 'Float' + end + + it "should convert a string into a float if casted as so" do + RootBeerFloat.new(:price => '12.50').price.should == 12.50 + RootBeerFloat.new(:price => '9').price.should == 9.0 + RootBeerFloat.new(:price => '-9').price.should == -9.0 + end + + it "should not convert a string if it's not a string that can be cast as a float" do + RootBeerFloat.new(:price => 'test').price.should == 'test' + end + + it "should work fine when a float is being passed" do + RootBeerFloat.new(:price => 9.99).price.should == 9.99 + end + + end + end end From 964526193bc56c68deba87745b71ed43e29317ae Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Thu, 16 Jul 2009 20:38:15 -0700 Subject: [PATCH 14/19] Optimized Model.count to run about 3x faster --- history.txt | 3 ++- lib/couchrest/mixins/design_doc.rb | 3 --- lib/couchrest/mixins/document_queries.rb | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/history.txt b/history.txt index 0035981..b434056 100644 --- a/history.txt +++ b/history.txt @@ -5,7 +5,8 @@ * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti) * Minor enhancements - + + * Optimized Model.count to run about 3x faster (Matt Aimonetti) * Added Float casting (Ryan Felton & Matt Aimonetti) == 0.30 diff --git a/lib/couchrest/mixins/design_doc.rb b/lib/couchrest/mixins/design_doc.rb index 4ed6fdf..661b6ef 100644 --- a/lib/couchrest/mixins/design_doc.rb +++ b/lib/couchrest/mixins/design_doc.rb @@ -37,9 +37,6 @@ module CouchRest if (doc['couchrest-type'] == '#{self.to_s}') { emit(null,1); } - }", - 'reduce' => "function(keys, values) { - return sum(values); }" } } diff --git a/lib/couchrest/mixins/document_queries.rb b/lib/couchrest/mixins/document_queries.rb index 3ea516c..defff3d 100644 --- a/lib/couchrest/mixins/document_queries.rb +++ b/lib/couchrest/mixins/document_queries.rb @@ -19,9 +19,7 @@ module CouchRest # equal to the name of the current class. Takes the standard set of # CouchRest::Database#view options def count(opts = {}, &block) - result = all({:reduce => true}.merge(opts), &block)['rows'] - return 0 if result.empty? - result.first['value'] + all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows'] end # Load the first document that have the "couchrest-type" field equal to From 51408990411411f3bf0826cdd0c042f9ccc0c7c7 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Fri, 17 Jul 2009 00:12:33 -0700 Subject: [PATCH 15/19] Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create --- history.txt | 3 ++- lib/couchrest/more/extended_document.rb | 19 ++++++++++++++++++- spec/couchrest/more/extended_doc_spec.rb | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/history.txt b/history.txt index b434056..f691eae 100644 --- a/history.txt +++ b/history.txt @@ -3,10 +3,11 @@ * Major enhancements * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti) + * Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create (Matt Aimonetti) * Minor enhancements - * Optimized Model.count to run about 3x faster (Matt Aimonetti) + * Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti) * Added Float casting (Ryan Felton & Matt Aimonetti) == 0.30 diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 14006ed..7fd067a 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -52,8 +52,25 @@ module CouchRest end end - + # Defines an instance and save it directly to the database + # + # ==== Returns + # returns the reloaded document + def self.create(options) + instance = new(options) + instance.create + instance + end + # Defines an instance and save it directly to the database + # + # ==== Returns + # returns the reloaded document or raises an exception + def self.create!(options) + instance = new(options) + instance.create! + instance + end # Automatically set updated_at and created_at fields # on the document whenever saving occurs. CouchRest uses a pretty diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 618ee0c..5d71d7f 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -97,6 +97,22 @@ describe "ExtendedDocument" do end end + describe "creating a new document" do + it "should instantialize and save a document" do + article = Article.create(:title => 'my test') + article.title.should == 'my test' + article.should_not be_new_document + end + + it "should trigger the create callbacks" do + doc = WithCallBacks.create(:name => 'my other test') + doc.run_before_create.should be_true + doc.run_after_create.should be_true + doc.run_before_save.should be_true + doc.run_after_save.should be_true + end + end + describe "update attributes without saving" do before(:each) do a = Article.get "big-bad-danger" rescue nil From 142989a80d84d5712311f48c2afd160e34f06f20 Mon Sep 17 00:00:00 2001 From: Arnaud Berthomier Date: Mon, 13 Jul 2009 17:58:48 +0800 Subject: [PATCH 16/19] Dont die on empty results Signed-off-by: Matt Aimonetti --- lib/couchrest/mixins/collection.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index 3af8d74..b492dbe 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -209,12 +209,14 @@ module CouchRest def remember_where_we_left_off(results, page) last_row = results['rows'].last - @last_key = last_row['key'] - @last_docid = last_row['id'] + if last_row + @last_key = last_row['key'] + @last_docid = last_row['id'] + end @last_page = page end end end end -end \ No newline at end of file +end From a23ab5ab5a9314215dcad7ca86525385b3df9fc1 Mon Sep 17 00:00:00 2001 From: Aaron Quint Date: Tue, 19 May 2009 12:15:42 +0800 Subject: [PATCH 17/19] Add init.rb for easy usage as a Rails plugin (Makes for easy submodule-ing) Signed-off-by: Matt Aimonetti --- init.rb | 1 + 1 file changed, 1 insertion(+) create mode 100644 init.rb diff --git a/init.rb b/init.rb new file mode 100644 index 0000000..976b492 --- /dev/null +++ b/init.rb @@ -0,0 +1 @@ +require File.join(File.dirname(__FILE__),'lib', 'couchrest.rb') \ No newline at end of file From 367bbd6f70859b48f47a671218d3f1f11c0a02a1 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Fri, 17 Jul 2009 10:53:00 -0700 Subject: [PATCH 18/19] updated the history.txt file --- history.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/history.txt b/history.txt index f691eae..1d886de 100644 --- a/history.txt +++ b/history.txt @@ -7,6 +7,8 @@ * Minor enhancements + * Added an init.rb file for easy usage as a Rails plugin (Aaron Quint) + * Bug fix: pagination shouldn't die on empty results (Arnaud Berthomier) * Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti) * Added Float casting (Ryan Felton & Matt Aimonetti) From 6c0d74717c4e9cbccb7a0b26b7b129392e333848 Mon Sep 17 00:00:00 2001 From: Matt Aimonetti Date: Fri, 17 Jul 2009 11:07:23 -0700 Subject: [PATCH 19/19] updated the readme --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4ef2748..d9cc490 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,17 @@ Note: CouchRest only support CouchDB 0.9.0 or newer. ## Easy Install -Easy Install is moving to RubyForge, heads up for the gem. + $ sudo gem install couchrest + +Alternatively, you can install from Github: + + $ gem sources -a http://gems.github.com (you only have to do this once) + $ sudo gem install mattetti-couchrest ### Relax, it's RESTful -The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper. -REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, -while still giving you more control than Open-URI. I recommend it anytime -you’re interfacing with a well-defined web service. +CouchRest rests on top of a HTTP abstraction layer using by default Heroku’s excellent REST Client Ruby HTTP wrapper. +Other adapters can be added to support more http libraries. ### Running the Specs @@ -27,7 +30,7 @@ The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support). -## Examples +## Examples (CouchRest Core) Quick Start: @@ -59,12 +62,50 @@ Creating and Querying Views: }) puts @db.view('first/test')['rows'].inspect -## CouchRest::Model -CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument +## CouchRest::ExtendedDocument +CouchRest::ExtendedDocument is a DSL/ORM for CouchDB. Basically, ExtendedDocument seats on top of CouchRest Core to add the concept of Model. +ExtendedDocument offers a lot of the usual ORM tools such as optional yet defined schema, validation, callbacks, pagination, casting and much more. -## CouchRest::ExtendedDocument +### Model example + +Check spec/couchrest/more and spec/fixtures/more for more examples + + class Article < CouchRest::ExtendedDocument + use_database DB + unique_id :slug + + view_by :date, :descending => true + view_by :user_id, :date + + view_by :tags, + :map => + "function(doc) { + if (doc['couchrest-type'] == 'Article' && doc.tags) { + doc.tags.forEach(function(tag){ + emit(tag, 1); + }); + } + }", + :reduce => + "function(keys, values, rereduce) { + return sum(values); + }" + + property :date + property :slug, :read_only => true + property :title + property :tags, :cast_as => ['String'] + + timestamps! + + save_callback :before, :generate_slug_from_title + + def generate_slug_from_title + self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document? + end + end ### Callbacks @@ -114,4 +155,11 @@ Basically, you can paginate through the articles starting by the letter a, 5 art Low level usage: Article.paginate(:design_doc => 'Article', :view_name => 'by_date', - :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true) \ No newline at end of file + :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true) + + +## Ruby on Rails + +CouchRest is compatible with rails and can even be used a Rails plugin. +However, you might be interested in the CouchRest companion rails project: +[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) \ No newline at end of file