Merge commit 'mattetti/master'

This commit is contained in:
Peter Gumeson 2009-07-19 00:01:07 -07:00
commit 1e44302d1a
25 changed files with 695 additions and 135 deletions

View file

@ -12,14 +12,17 @@ Note: CouchRest only support CouchDB 0.9.0 or newer.
## Easy Install ## 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 ### Relax, it's RESTful
The core of Couchrest is Herokus excellent REST Client Ruby HTTP wrapper. CouchRest rests on top of a HTTP abstraction layer using by default Herokus excellent REST Client Ruby HTTP wrapper.
REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, Other adapters can be added to support more http libraries.
while still giving you more control than Open-URI. I recommend it anytime
youre interfacing with a well-defined web service.
### Running the Specs ### 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` CouchRest install, from the project root directory run `rake`, or `autotest`
(requires RSpec and optionally ZenTest for autotest support). (requires RSpec and optionally ZenTest for autotest support).
## Examples ## Examples (CouchRest Core)
Quick Start: Quick Start:
@ -59,12 +62,50 @@ Creating and Querying Views:
}) })
puts @db.view('first/test')['rows'].inspect 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?
end
end
### Callbacks ### Callbacks
@ -84,12 +125,14 @@ CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted f
set_callback :save, :after, :after_method, :if => :condition set_callback :save, :after, :after_method, :if => :condition
set_callback :save, :around {|r| stuff; yield; stuff } set_callback :save, :around {|r| stuff; yield; stuff }
Or the new shorter version: Or the aliased short version:
before_save :before_method, :another_method before_save :before_method, :another_method
after_save :after_method, :another_method, :if => :condition after_save :after_method, :another_method, :if => :condition
around_save {|r| stuff; yield; stuff } around_save {|r| stuff; yield; stuff }
To halt the callback, simply return a :halt symbol in your callback method.
Check the mixin or the ExtendedDocument class to see how to implement your own callbacks. Check the mixin or the ExtendedDocument class to see how to implement your own callbacks.
### Casting ### Casting
@ -102,3 +145,32 @@ 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"] 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)
## 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)

View file

@ -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.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.has_rdoc = true
s.authors = ["J. Chris Anderson", "Matt Aimonetti"] 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["{examples,lib,spec,utils}/**/*"] -
Dir["spec/tmp"] Dir["spec/tmp"]
s.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) s.extra_rdoc_files = %w( README.md LICENSE THANKS.md )

View file

@ -2,7 +2,7 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{couchrest} 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.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti"] 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.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.email = %q{jchris@apache.org}
s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] 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", "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.has_rdoc = true
s.homepage = %q{http://github.com/jchris/couchrest} s.homepage = %q{http://github.com/jchris/couchrest}
s.require_paths = ["lib"] 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.} s.summary = %q{Lean and RESTful interface to CouchDB.}
if s.respond_to? :specification_version then if s.respond_to? :specification_version then

33
history.txt Normal file
View file

@ -0,0 +1,33 @@
== 0.31
* 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
* 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)
== 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/

1
init.rb Normal file
View file

@ -0,0 +1 @@
require File.join(File.dirname(__FILE__),'lib', 'couchrest.rb')

View file

@ -28,7 +28,7 @@ require 'couchrest/monkeypatches'
# = CouchDB, close to the metal # = CouchDB, close to the metal
module CouchRest module CouchRest
VERSION = '0.29' unless self.const_defined?("VERSION") VERSION = '0.30' unless self.const_defined?("VERSION")
autoload :Server, 'couchrest/core/server' autoload :Server, 'couchrest/core/server'
autoload :Database, 'couchrest/core/database' autoload :Database, 'couchrest/core/database'
@ -45,6 +45,7 @@ module CouchRest
autoload :ExtendedDocument, 'couchrest/more/extended_document' autoload :ExtendedDocument, 'couchrest/more/extended_document'
autoload :CastedModel, 'couchrest/more/casted_model' autoload :CastedModel, 'couchrest/more/casted_model'
require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction')
require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') require File.join(File.dirname(__FILE__), 'couchrest', 'mixins')
require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails) require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails)
@ -119,9 +120,9 @@ module CouchRest
} }
end end
# set proxy for RestClient to use # set proxy to use
def proxy url def proxy url
RestClient.proxy = url HttpAbstraction.proxy = url
end end
# ensure that a database exists # ensure that a database exists
@ -142,7 +143,7 @@ module CouchRest
def put(uri, doc = nil) def put(uri, doc = nil)
payload = doc.to_json if doc payload = doc.to_json if doc
begin begin
JSON.parse(RestClient.put(uri, payload)) JSON.parse(HttpAbstraction.put(uri, payload))
rescue Exception => e rescue Exception => e
if $DEBUG if $DEBUG
raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}" raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}"
@ -154,7 +155,7 @@ module CouchRest
def get(uri) def get(uri)
begin begin
JSON.parse(RestClient.get(uri), :max_nesting => false) JSON.parse(HttpAbstraction.get(uri), :max_nesting => false)
rescue => e rescue => e
if $DEBUG if $DEBUG
raise "Error while sending a GET request #{uri}\n: #{e}" raise "Error while sending a GET request #{uri}\n: #{e}"
@ -167,7 +168,7 @@ module CouchRest
def post uri, doc = nil def post uri, doc = nil
payload = doc.to_json if doc payload = doc.to_json if doc
begin begin
JSON.parse(RestClient.post(uri, payload)) JSON.parse(HttpAbstraction.post(uri, payload))
rescue Exception => e rescue Exception => e
if $DEBUG if $DEBUG
raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}" raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}"
@ -178,11 +179,11 @@ module CouchRest
end end
def delete uri def delete uri
JSON.parse(RestClient.delete(uri)) JSON.parse(HttpAbstraction.delete(uri))
end end
def copy uri, destination def copy uri, destination
JSON.parse(RestClient.copy(uri, {'Destination' => destination})) JSON.parse(HttpAbstraction.copy(uri, {'Destination' => destination}))
end end
def paramify_url url, params = {} def paramify_url url, params = {}

View file

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

View file

@ -58,7 +58,7 @@ module CouchRest
keys = params.delete(:keys) keys = params.delete(:keys)
funcs = funcs.merge({:keys => keys}) if keys funcs = funcs.merge({:keys => keys}) if keys
url = CouchRest.paramify_url "#{@root}/_temp_view", params 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 end
# backwards compatibility is a plus # backwards compatibility is a plus
@ -100,11 +100,8 @@ module CouchRest
# GET an attachment directly from CouchDB # GET an attachment directly from CouchDB
def fetch_attachment(doc, name) def fetch_attachment(doc, name)
# slug = escape_docid(docid)
# name = CGI.escape(name)
uri = url_for_attachment(doc, name) uri = url_for_attachment(doc, name)
RestClient.get uri HttpAbstraction.get uri
# "#{@uri}/#{slug}/#{name}"
end end
# PUT an attachment directly to CouchDB # PUT an attachment directly to CouchDB
@ -112,14 +109,14 @@ module CouchRest
docid = escape_docid(doc['_id']) docid = escape_docid(doc['_id'])
name = CGI.escape(name) name = CGI.escape(name)
uri = url_for_attachment(doc, name) uri = url_for_attachment(doc, name)
JSON.parse(RestClient.put(uri, file, options)) JSON.parse(HttpAbstraction.put(uri, file, options))
end end
# DELETE an attachment directly from CouchDB # DELETE an attachment directly from CouchDB
def delete_attachment doc, name def delete_attachment doc, name
uri = url_for_attachment(doc, name) uri = url_for_attachment(doc, name)
# this needs a rev # this needs a rev
JSON.parse(RestClient.delete(uri)) JSON.parse(HttpAbstraction.delete(uri))
end end
# Save a document to CouchDB. This will use the <tt>_id</tt> field from # Save a document to CouchDB. This will use the <tt>_id</tt> field from
@ -146,7 +143,7 @@ module CouchRest
slug = escape_docid(doc['_id']) slug = escape_docid(doc['_id'])
begin begin
CouchRest.put "#{@root}/#{slug}", doc CouchRest.put "#{@root}/#{slug}", doc
rescue RestClient::ResourceNotFound rescue HttpAbstraction::ResourceNotFound
p "resource not found when saving even tho an id was passed" p "resource not found when saving even tho an id was passed"
slug = doc['_id'] = @server.next_uuid slug = doc['_id'] = @server.next_uuid
CouchRest.put "#{@root}/#{slug}", doc CouchRest.put "#{@root}/#{slug}", doc
@ -252,7 +249,7 @@ module CouchRest
def recreate! def recreate!
delete! delete!
create! create!
rescue RestClient::ResourceNotFound rescue HttpAbstraction::ResourceNotFound
ensure ensure
create! create!
end end

View file

@ -3,10 +3,6 @@ require 'delegate'
module CouchRest module CouchRest
class Document < Response class Document < Response
include CouchRest::Mixins::Attachments include CouchRest::Mixins::Attachments
# def self.inherited(subklass)
# subklass.send(:extlib_inheritable_accessor, :database)
# end
extlib_inheritable_accessor :database extlib_inheritable_accessor :database
attr_accessor :database attr_accessor :database
@ -30,6 +26,7 @@ module CouchRest
def new? def new?
!rev !rev
end end
alias :new_document? :new?
# Saves the document to the db using create or update. Also runs the :save # Saves the document to the db using create or update. Also runs the :save
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on

View file

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

View file

@ -0,0 +1,222 @@
module CouchRest
module Mixins
module Collection
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Creates a new class method, find_all_<collection_name>, 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, design_doc, view_name, view_options)
class_eval <<-END, __FILE__, __LINE__ + 1
def self.find_all_#{collection_name}(options = {})
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)
end
private
def create_collection_proxy(options)
design_doc, view_name, view_options = parse_view_options(options)
CollectionProxy.new(@database, design_doc, view_name, view_options, self)
end
def parse_view_options(options)
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?
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)
[design_doc, 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$)/ }
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?
@database = database
@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
@view_name = "#{design_doc}/#{view_name}"
end
end
# See Collection.paginate
def paginate(options = {})
page, per_page = parse_options(options)
results = @database.view(@view_name, pagination_options(page, per_page))
remember_where_we_left_off(results, page)
convert_to_container_array(results)
end
# See Collection.paginated_each
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?
results = @database.view(@view_name, @view_options)
@target = convert_to_container_array(results)
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(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)
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)
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
def remember_where_we_left_off(results, page)
last_row = results['rows'].last
if last_row
@last_key = last_row['key']
@last_docid = last_row['id']
end
@last_page = page
end
end
end
end
end

View file

@ -37,9 +37,6 @@ module CouchRest
if (doc['couchrest-type'] == '#{self.to_s}') { if (doc['couchrest-type'] == '#{self.to_s}') {
emit(null,1); emit(null,1);
} }
}",
'reduce' => "function(keys, values) {
return sum(values);
}" }"
} }
} }

View file

@ -19,9 +19,7 @@ module CouchRest
# equal to the name of the current class. Takes the standard set of # equal to the name of the current class. Takes the standard set of
# CouchRest::Database#view options # CouchRest::Database#view options
def count(opts = {}, &block) def count(opts = {}, &block)
result = all({:reduce => true}.merge(opts), &block)['rows'] all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
return 0 if result.empty?
result.first['value']
end end
# Load the first document that have the "couchrest-type" field equal to # Load the first document that have the "couchrest-type" field equal to

View file

@ -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__), 'validation')
require File.join(File.dirname(__FILE__), 'extended_attachments') require File.join(File.dirname(__FILE__), 'extended_attachments')
require File.join(File.dirname(__FILE__), 'class_proxy') require File.join(File.dirname(__FILE__), 'class_proxy')
require File.join(File.dirname(__FILE__), 'collection')

View file

@ -1,6 +1,25 @@
require 'time' require 'time'
require File.join(File.dirname(__FILE__), '..', 'more', 'property') 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 CouchRest
module Mixins module Mixins
module Properties module Properties
@ -65,6 +84,7 @@ module CouchRest
end end
associate_casted_to_parent(self[property.name], assigned) associate_casted_to_parent(self[property.name], assigned)
end end
end end
def associate_casted_to_parent(casted, assigned) def associate_casted_to_parent(casted, assigned)
@ -73,8 +93,12 @@ module CouchRest
end end
def convert_property_value(property, klass, value) def convert_property_value(property, klass, value)
if ((property.init_method == 'new') && klass.to_s == 'Time') if ((property.init_method == 'new') && klass.to_s == 'Time')
value.is_a?(String) ? Time.parse(value.dup) : value # Using custom time parsing method because Ruby's default method is toooo slow
value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
# Float instances don't get initialized with #new
elsif ((property.init_method == 'new') && klass.to_s == 'Float')
cast_float(value)
else else
klass.send(property.init_method, value.dup) klass.send(property.init_method, value.dup)
end end
@ -87,6 +111,14 @@ module CouchRest
cast_property(property, true) cast_property(property, true)
end end
def cast_float(value)
begin
Float(value)
rescue
value
end
end
module ClassMethods module ClassMethods
def property(name, options={}) def property(name, options={})
@ -146,4 +178,4 @@ module CouchRest
end end
end end
end end

View file

@ -72,7 +72,7 @@ module CouchRest
# #
# To understand the capabilities of this view system more completely, # To understand the capabilities of this view system more completely,
# it is recommended that you read the RSpec file at # it is recommended that you read the RSpec file at
# <tt>spec/core/model_spec.rb</tt>. # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
def view_by(*keys) def view_by(*keys)
opts = keys.pop if keys.last.is_a?(Hash) opts = keys.pop if keys.last.is_a?(Hash)
@ -124,14 +124,6 @@ module CouchRest
# potentially large indexes. # potentially large indexes.
def cleanup_design_docs!(db = database) def cleanup_design_docs!(db = database)
save_design_doc_on(db) 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 end
private private
@ -141,8 +133,12 @@ module CouchRest
fetch_view(db, name, opts, &block) fetch_view(db, name, opts, &block)
else else
begin begin
view = fetch_view db, name, opts.merge({:include_docs => true}), &block if block.nil?
view['rows'].collect{|r|new(r['doc'])} if view['rows'] 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 rescue
# fallback for old versions of couchdb that don't # fallback for old versions of couchdb that don't
# have include_docs support # have include_docs support
@ -158,7 +154,7 @@ module CouchRest
begin begin
design_doc.view_on(db, view_name, opts, &block) design_doc.view_on(db, view_name, opts, &block)
# the design doc may not have been saved yet on this database # the design doc may not have been saved yet on this database
rescue RestClient::ResourceNotFound => e rescue HttpAbstraction::ResourceNotFound => e
if retryable if retryable
save_design_doc_on(db) save_design_doc_on(db)
retryable = false retryable = false

View file

@ -1,5 +1,6 @@
require File.join(File.dirname(__FILE__), 'support', 'class') require File.join(File.dirname(__FILE__), 'support', 'class')
require File.join(File.dirname(__FILE__), 'support', 'blank') 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. # This file must be loaded after the JSON gem and any other library that beats up the Time class.
class Time class Time
@ -38,7 +39,7 @@ if RUBY_VERSION.to_f < 1.9
if IO.select([@io], nil, nil, @read_timeout) if IO.select([@io], nil, nil, @read_timeout)
retry retry
else else
raise Timeout::TimeoutError raise Timeout::Error
end end
end end
else else
@ -50,63 +51,63 @@ if RUBY_VERSION.to_f < 1.9
end end
end end
module RestClient # module RestClient
def self.copy(url, headers={}) # # def self.copy(url, headers={})
Request.execute(:method => :copy, # # Request.execute(:method => :copy,
:url => url, # # :url => url,
:headers => headers) # # :headers => headers)
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 # # class Request
# http = Thread.current[:connection] # #
# http.read_timeout = @timeout if @timeout # # def establish_connection(uri)
# # # Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?)
# begin # # p net_http_class
# res = http.request(req, payload) # # net = net_http_class.new(uri.host, uri.port)
# rescue # # net.use_ssl = uri.is_a?(URI::HTTPS)
# p "Net::HTTP connection failed, reconnecting" # # net.verify_mode = OpenSSL::SSL::VERIFY_NONE
# establish_connection(uri) # # Thread.current[:connection] = net
# http = Thread.current[:connection] # # Thread.current[:connection].start
# require 'ruby-debug' # # Thread.current[:connection]
# req.body_stream = nil # # end
# # #
# res = http.request(req, payload) # # def transmit(uri, req, payload)
# display_log response_log(res) # # setup_credentials(req)
# result res # #
# else # # Thread.current[:host] ||= uri.host
# display_log response_log(res) # # Thread.current[:port] ||= uri.port
# process_result res # #
# end # # if (Thread.current[:connection].nil? || (Thread.current[:host] != uri.host))
# # # p "establishing a connection"
# rescue EOFError # # establish_connection(uri)
# raise RestClient::ServerBrokeConnection # # end
# rescue Timeout::Error # #
# raise RestClient::RequestTimeout # # display_log request_log
# end # # http = Thread.current[:connection]
# end # # http.read_timeout = @timeout if @timeout
# #
end # # 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

View file

@ -13,10 +13,11 @@ module CouchRest
include CouchRest::Mixins::DesignDoc include CouchRest::Mixins::DesignDoc
include CouchRest::Mixins::ExtendedAttachments include CouchRest::Mixins::ExtendedAttachments
include CouchRest::Mixins::ClassProxy include CouchRest::Mixins::ClassProxy
include CouchRest::Mixins::Collection
def self.subclasses def self.subclasses
@subclasses ||= [] @subclasses ||= []
end end
def self.inherited(subklass) def self.inherited(subklass)
subklass.send(:include, CouchRest::Mixins::Properties) subklass.send(:include, CouchRest::Mixins::Properties)
@ -51,6 +52,26 @@ module CouchRest
end end
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 <tt>updated_at</tt> and <tt>created_at</tt> fields # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
# on the document whenever saving occurs. CouchRest uses a pretty # on the document whenever saving occurs. CouchRest uses a pretty
# decent time format by default. See Time#to_json # decent time format by default. See Time#to_json

View file

@ -40,7 +40,7 @@ module CouchRest
value = target.send(field_name) value = target.send(field_name)
return true if @options[:allow_nil] && value.nil? 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] error_message = @options[:message]
precision = @options[:precision] precision = @options[:precision]

View file

@ -191,7 +191,7 @@ describe CouchRest do
describe "using a proxy for RestClient connections" do describe "using a proxy for RestClient connections" do
it "should set proxy url for RestClient" do it "should set proxy url for RestClient" do
CouchRest.proxy 'http://localhost:8888/' 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.host.should eql( 'localhost' )
proxy_uri.port.should eql( 8888 ) proxy_uri.port.should eql( 8888 )
CouchRest.proxy nil CouchRest.proxy nil

View file

@ -690,7 +690,7 @@ describe CouchRest::Database do
it "should recreate a db even tho it doesn't exist" do it "should recreate a db even tho it doesn't exist" do
@cr.databases.should_not include(@db2.name) @cr.databases.should_not include(@db2.name)
@db2.recreate! begin @db2.recreate! rescue nil end
@cr.databases.should include(@db2.name) @cr.databases.should include(@db2.name)
end end

View file

@ -121,15 +121,28 @@ describe "ExtendedDocument" do
end end
describe "a new model" do describe "a new model" do
it "should be a new_record" do it "should be a new document" do
@obj = Basic.new @obj = Basic.new
@obj.rev.should be_nil @obj.rev.should be_nil
@obj.should be_new @obj.should be_new
@obj.should be_new_document
@obj.should be_new_record
end end
it "should be a new_document" do end
@obj = Basic.new
@obj.rev.should be_nil describe "creating a new document" do
@obj.should be_new it "should instantialize and save a document" do
article = Article.create(:title => 'my test')
article.title.should == 'my test'
article.should_not be_new
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
end end

View file

@ -121,7 +121,7 @@ describe "ExtendedDocument views" do
describe "a model class not tied to a database" do describe "a model class not tied to a database" do
before(:all) do before(:all) do
reset_test_db! reset_test_db!
@db = DB @db = DB
%w{aaa bbb ddd eee}.each do |title| %w{aaa bbb ddd eee}.each do |title|
u = Unattached.new(:title => title) u = Unattached.new(:title => title)
u.database = @db u.database = @db
@ -133,14 +133,15 @@ describe "ExtendedDocument views" do
lambda{Unattached.all}.should raise_error lambda{Unattached.all}.should raise_error
end end
it "should query all" do it "should query all" do
rs = Unattached.all :database=>@db Unattached.cleanup_design_docs!(@db)
rs = Unattached.all :database => @db
rs.length.should == 4 rs.length.should == 4
end end
it "should barf on query if no database given" do it "should barf on query if no database given" do
lambda{Unattached.view :by_title}.should raise_error lambda{Unattached.view :by_title}.should raise_error
end end
it "should make the design doc upon first query" do 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 = Unattached.design_doc
doc['views']['all']['map'].should include('Unattached') doc['views']['all']['map'].should include('Unattached')
end end
@ -157,7 +158,7 @@ describe "ExtendedDocument views" do
things = [] things = []
Unattached.view(:by_title, :database=>@db) do |thing| Unattached.view(:by_title, :database=>@db) do |thing|
things << thing things << thing
end end
things[0]["doc"]["title"].should =='aaa' things[0]["doc"]["title"].should =='aaa'
end end
it "should yield with by_key method" do it "should yield with by_key method" do
@ -337,5 +338,78 @@ describe "ExtendedDocument views" do
Article.design_doc["views"].keys.should include("by_updated_at") Article.design_doc["views"].keys.should include("by_updated_at")
end end
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 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
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
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, 'Article', '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 end

View file

@ -142,7 +142,29 @@ describe "ExtendedDocument properties" do
@event['occurs_at'].should be_an_instance_of(Time) @event['occurs_at'].should be_an_instance_of(Time)
end end
end 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 end
describe "a newly created casted model" do describe "a newly created casted model" do

View file

@ -20,7 +20,7 @@ class Basic < CouchRest::ExtendedDocument
end end
def reset_test_db! def reset_test_db!
DB.recreate! rescue nil DB.recreate! rescue nil
DB DB
end end