diff --git a/.gitignore b/.gitignore index b07b413..e1d9273 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store html/* pkg +*.swp +couchrest.gemspec diff --git a/README.md b/README.md index 73e9387..ddc7460 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,6 @@ Note: CouchRest only support CouchDB 0.9.0 or newer. $ 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 couchrest-couchrest - ### Relax, it's RESTful CouchRest rests on top of a HTTP abstraction layer using by default Heroku’s excellent REST Client Ruby HTTP wrapper. @@ -30,152 +25,19 @@ 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 (CouchRest Core) +## Docs -Quick Start: +API: [http://rdoc.info/projects/couchrest/couchrest](http://rdoc.info/projects/couchrest/couchrest) - # with !, it creates the database if it doesn't already exist - @db = CouchRest.database!("http://127.0.0.1:5984/couchrest-test") - response = @db.save_doc({:key => 'value', 'another key' => 'another value'}) - doc = @db.get(response['id']) - puts doc.inspect +Check the wiki for documentation and examples [http://wiki.github.com/couchrest/couchrest](http://wiki.github.com/couchrest/couchrest) -Bulk Save: +## Contact - @db.bulk_save([ - {"wild" => "and random"}, - {"mild" => "yet local"}, - {"another" => ["set","of","keys"]} - ]) - # returns ids and revs of the current docs - puts @db.documents.inspect +Please post bugs, suggestions and patches to the bug tracker at . -Creating and Querying Views: +Follow us on Twitter: http://twitter.com/couchrest - @db.save_doc({ - "_id" => "_design/first", - :views => { - :test => { - :map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}" - } - } - }) - puts @db.view('first/test')['rows'].inspect - - -## 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. - -### 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 - -`CouchRest::ExtendedDocuments` instances have 4 callbacks already defined for you: - `:validate`, `:create`, `:save`, `:update` and `:destroy` - -`CouchRest::CastedModel` instances have 1 callback already defined for you: - `:validate` - -Define your callback as follows: - - set_callback :save, :before, :generate_slug_from_name - -CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted from Rails 3, here are some simple usage examples: - - set_callback :save, :before, :before_method - set_callback :save, :after, :after_method, :if => :condition - set_callback :save, :around {|r| stuff; yield; stuff } - - Or the aliased short version: - - before_save :before_method, :another_method - after_save :after_method, :another_method, :if => :condition - 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. - -### Properties - - property :last_name, :alias => :family_name - property :read_only_value, :read_only => true - property :name, :length => 4...20 - property :price, :type => Integer - -### Casting - -Often, you will want to store multiple objects within a document, to be able to retrieve your objects when you load the document, -you can define some casting rules. - - property :casted_attribute, :cast_as => 'WithCastedModelMixin' - property :keywords, :cast_as => ["String"] - property :occurs_at, :cast_as => 'Time', :send => 'parse - property :end_date, :cast_as => 'Date', :send => 'parse - -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) +Also, check http://twitter.com/#search?q=%23couchrest ## Ruby on Rails diff --git a/Rakefile b/Rakefile index f5650f6..6936814 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,7 @@ require 'rake' require "rake/rdoctask" -require 'rake/gempackagetask' require File.join(File.expand_path(File.dirname(__FILE__)),'lib','couchrest') - begin require 'spec/rake/spectask' rescue LoadError @@ -14,41 +12,26 @@ EOS exit(0) end -spec = Gem::Specification.new do |s| - s.name = "couchrest" - s.version = CouchRest::VERSION - s.date = "2008-11-22" - s.summary = "Lean and RESTful interface to CouchDB." - s.email = "jchris@apache.org" - s.homepage = "http://github.com/jchris/couchrest" - 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 history.txt) + - Dir["{examples,lib,spec,utils}/**/*"] - - Dir["spec/tmp"] - s.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) - s.require_path = "lib" - s.add_dependency("rest-client", ">= 0.5") - s.add_dependency("mime-types", ">= 1.15") -end - - -desc "Create .gemspec file (useful for github)" -task :gemspec do - filename = "#{spec.name}.gemspec" - File.open(filename, "w") do |f| - f.puts spec.to_ruby +begin + require 'jeweler' + Jeweler::Tasks.new do |gemspec| + gemspec.name = "couchrest" + gemspec.summary = "Lean and RESTful interface to CouchDB." + gemspec.description = "CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments." + gemspec.email = "jchris@apache.org" + gemspec.homepage = "http://github.com/couchrest/couchrest" + gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos"] + gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md ) + gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"] + gemspec.has_rdoc = true + gemspec.add_dependency("rest-client", ">= 0.5") + gemspec.add_dependency("mime-types", ">= 1.15") + gemspec.version = CouchRest::VERSION + gemspec.date = "2008-11-22" + gemspec.require_path = "lib" end -end - -Rake::GemPackageTask.new(spec) do |pkg| - pkg.gem_spec = spec -end - -desc "Install the gem locally" -task :install => [:package] do - sh %{sudo gem install pkg/couchrest-#{CouchRest::VERSION}} +rescue LoadError + puts "Jeweler not available. Install it with: gem install jeweler" end desc "Run all specs" @@ -73,3 +56,12 @@ end desc "Run the rspec" task :default => :spec + +module Rake + def self.remove_task(task_name) + Rake.application.instance_variable_get('@tasks').delete(task_name.to_s) + end +end + +Rake.remove_task("github:release") +Rake.remove_task("release") \ No newline at end of file diff --git a/THANKS.md b/THANKS.md index 643cf5e..08edeec 100644 --- a/THANKS.md +++ b/THANKS.md @@ -14,6 +14,6 @@ changes. A list of these people is included below. * Simon Rozet (simon /at/ rozet /dot/ name) * [Marcos Tapajós](http://tapajos.me) -Patches are welcome. The primary source for this software project is [on Github](http://github.com/jchris/couchrest/tree/master) +Patches are welcome. The primary source for this software project is [on Github](http://github.com/couchrest/couchrest) A lot of people have active forks - thank you all - even the patches I don't end up using are helpful. diff --git a/couchrest.gemspec b/couchrest.gemspec deleted file mode 100644 index 32b9da0..0000000 --- a/couchrest.gemspec +++ /dev/null @@ -1,35 +0,0 @@ -# -*- encoding: utf-8 -*- - -Gem::Specification.new do |s| - s.name = %q{couchrest} - s.version = "0.34" - - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["J. Chris Anderson", "Matt Aimonetti"] - s.date = %q{2008-11-22} - 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", "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/adapters", "lib/couchrest/core/adapters/restclient.rb", "lib/couchrest/core/database.rb", "lib/couchrest/core/design.rb", "lib/couchrest/core/document.rb", "lib/couchrest/core/http_abstraction.rb", "lib/couchrest/core/response.rb", "lib/couchrest/core/rest_api.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/middlewares", "lib/couchrest/middlewares/logger.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"] - s.rubygems_version = %q{1.3.1} - s.summary = %q{Lean and RESTful interface to CouchDB.} - - if s.respond_to? :specification_version then - current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION - s.specification_version = 2 - - if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, [">= 0.5"]) - s.add_runtime_dependency(%q, [">= 1.15"]) - else - s.add_dependency(%q, [">= 0.5"]) - s.add_dependency(%q, [">= 1.15"]) - end - else - s.add_dependency(%q, [">= 0.5"]) - s.add_dependency(%q, [">= 1.15"]) - end -end diff --git a/github_gemtest.rb b/github_gemtest.rb deleted file mode 100644 index f15aa0b..0000000 --- a/github_gemtest.rb +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env ruby -require 'yaml' - -if ARGV.size < 1 - puts "Usage: github-test.rb my-project.gemspec" - exit -end - -require 'rubygems/specification' -data = File.read(ARGV[0]) -spec = nil - -if data !~ %r{!ruby/object:Gem::Specification} - Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join -else - spec = YAML.load(data) -end - -puts spec -puts "OK" \ No newline at end of file diff --git a/history.txt b/history.txt index 5ce94b2..3ac3c2c 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,50 @@ +== Next Version + +* Major enhancements + +* Minor enhancements + +== 0.35 + +* Major enhancements + * CouchRest::ExtendedDocument allow chaining the inherit class callback (Kenneth Kalmer) - http://github.com/couchrest/couchrest/issues#issue/8 + +* Minor enhancements + * Fix attachment bug (Johannes Jörg Schmidt) + * Fix create database exception bug (Damien Mathieu) + * Compatible with restclient >= 1.4.0 new responses (Julien Kirch) + * Bug fix: Attribute protection no longer strips attributes coming from the database (Will Leinweber) + * Bug fix: Remove double CGI escape when PUTting an attachment (nzoschke) + * Bug fix: Changing Class proxy to set database on result sets (Peter Gumeson) + * Bug fix: Updated time regexp (Nolan Darilek) + * Added an update_doc method to database to handle conflicts during atomic updates. (Pierre Larochelle) + * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton) + +== 0.34 + +* Major enhancements + + * Added support for https database URIs. (Mathias Meyer) + * Changing some validations to be compatible with activemodel. (Marcos Tapajós) + * Adds attribute protection to properties. (Will Leinweber) + * Improved CouchRest::Database#save_doc, added "batch" mode to significantly speed up saves at cost of lower durability gurantees. (Igal Koshevoy) + * Added CouchRest::Database#bulk_save_doc and #batch_save_doc as human-friendlier wrappers around #save_doc. (Igal Koshevoy) + +* Minor enhancements + + * Fix content_type handling for attachments + * Fixed a bug in the pagination code that caused it to paginate over records outside of the scope of the view parameters.(John Wood) + * Removed amount_pages calculation for the pagination collection, since it cannot be reliably calculated without a view.(John Wood) + * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton) + * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/1 (Marcos Tapajós) + * Removed the Database class deprecation notices (Matt Aimonetti) + * Adding support to :cast_as => 'Date'. (Marcos Tapajós) + * Improve documentation (Marcos Tapajós) + * Streamer fixes (Julien Sanchez) + * Fix Save on Document & ExtendedDocument crashed if bulk (Julien Sanchez) + * Fix Initialization of ExtendentDocument model shouldn't failed on a nil value in argument (deepj) + * Change to use Jeweler and Gemcutter (Marcos Tapajós) + == 0.33 * Major enhancements @@ -60,4 +107,4 @@ --- 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/couchrest/couchrest/commits/master/ \ No newline at end of file +You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/ diff --git a/lib/couchrest.rb b/lib/couchrest.rb index b69728b..e88d5a8 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.34' unless self.const_defined?("VERSION") + VERSION = '0.35' unless self.const_defined?("VERSION") autoload :Server, 'couchrest/core/server' autoload :Database, 'couchrest/core/database' @@ -96,14 +96,14 @@ module CouchRest def parse url case url - when /^http:\/\/(.*)\/(.*)\/(.*)/ + when /^https?:\/\/(.*)\/(.*)\/(.*)/ host = $1 db = $2 docid = $3 - when /^http:\/\/(.*)\/(.*)/ + when /^https?:\/\/(.*)\/(.*)/ host = $1 db = $2 - when /^http:\/\/(.*)/ + when /^https?:\/\/(.*)/ host = $1 when /(.*)\/(.*)\/(.*)/ host = $1 diff --git a/lib/couchrest/core/adapters/restclient.rb b/lib/couchrest/core/adapters/restclient.rb index ed02228..1be0433 100644 --- a/lib/couchrest/core/adapters/restclient.rb +++ b/lib/couchrest/core/adapters/restclient.rb @@ -10,25 +10,25 @@ module RestClientAdapter end def get(uri, headers={}) - RestClient.get(uri, headers) + RestClient.get(uri, headers).to_s end def post(uri, payload, headers={}) - RestClient.post(uri, payload, headers) + RestClient.post(uri, payload, headers).to_s end def put(uri, payload, headers={}) - RestClient.put(uri, payload, headers) + RestClient.put(uri, payload, headers).to_s end def delete(uri, headers={}) - RestClient.delete(uri, headers) + RestClient.delete(uri, headers).to_s end def copy(uri, headers) RestClient::Request.execute( :method => :copy, :url => uri, - :headers => headers) + :headers => headers).to_s end end diff --git a/lib/couchrest/core/database.rb b/lib/couchrest/core/database.rb index 2fb722a..36597fc 100644 --- a/lib/couchrest/core/database.rb +++ b/lib/couchrest/core/database.rb @@ -107,7 +107,6 @@ module CouchRest # PUT an attachment directly to CouchDB def put_attachment(doc, name, file, options = {}) docid = escape_docid(doc['_id']) - name = CGI.escape(name) uri = url_for_attachment(doc, name) JSON.parse(HttpAbstraction.put(uri, file, options)) end @@ -129,7 +128,7 @@ module CouchRest end end end - + # Save a document to CouchDB. This will use the _id field from # the document as the id for PUT, or request a new UUID from CouchDB, if # no _id is present on the document. IDs are attached to @@ -139,13 +138,25 @@ module CouchRest # # If bulk is true (false by default) the document is cached for bulk-saving later. # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save. - def save_doc(doc, bulk = false) + # + # If batch is true (false by default) the document is saved in + # batch mode, "used to achieve higher throughput at the cost of lower + # guarantees. When [...] sent using this option, it is not immediately + # written to disk. Instead it is stored in memory on a per-user basis for a + # second or so (or the number of docs in memory reaches a certain point). + # After the threshold has passed, the docs are committed to disk. Instead + # of waiting for the doc to be written to disk before responding, CouchDB + # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable + # for crucial data, but it ideal for applications like logging which can + # accept the risk that a small proportion of updates could be lost due to a + # crash." + def save_doc(doc, bulk = false, batch = false) if doc['_attachments'] doc['_attachments'] = encode_attachments(doc['_attachments']) end if bulk @bulk_save_cache << doc - return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit + bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit return {"ok" => true} # Compatibility with Document#save elsif !bulk && @bulk_save_cache.length > 0 bulk_save @@ -153,7 +164,9 @@ module CouchRest result = if doc['_id'] slug = escape_docid(doc['_id']) begin - CouchRest.put "#{@root}/#{slug}", doc + uri = "#{@root}/#{slug}" + uri << "?batch=ok" if batch + CouchRest.put uri, doc rescue HttpAbstraction::ResourceNotFound p "resource not found when saving even tho an id was passed" slug = doc['_id'] = @server.next_uuid @@ -175,6 +188,15 @@ module CouchRest result end + # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument. + def bulk_save_doc(doc) + save_doc(doc, true) + end + + # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument. + def batch_save_doc(doc) + save_doc(doc, false, true) + end # POST an array of documents to CouchDB. If any of the documents are # missing ids, supply one from the uuid cache. @@ -227,6 +249,33 @@ module CouchRest CouchRest.copy "#{@root}/#{slug}", destination end + # Updates the given doc by yielding the current state of the doc + # and trying to update update_limit times. Returns the new doc + # if the doc was successfully updated without hitting the limit + def update_doc(doc_id, params = {}, update_limit=10) + resp = {'ok' => false} + new_doc = nil + last_fail = nil + + until resp['ok'] or update_limit <= 0 + doc = self.get(doc_id, params) # grab the doc + new_doc = yield doc # give it to the caller to be updated + begin + resp = self.save_doc new_doc # try to PUT the updated doc into the db + rescue RestClient::RequestFailed => e + if e.http_code == 409 # Update collision + update_limit -= 1 + last_fail = e + else # some other error + raise e + end + end + end + + raise last_fail unless resp['ok'] + new_doc + end + # Compact the database, removing old document revisions and optimizing space use. def compact! CouchRest.post "#{@root}/_compact" @@ -242,7 +291,7 @@ module CouchRest def recreate! delete! create! - rescue HttpAbstraction::ResourceNotFound + rescue RestClient::ResourceNotFound ensure create! end diff --git a/lib/couchrest/helper/streamer.rb b/lib/couchrest/helper/streamer.rb index 6d48ca0..35eb824 100644 --- a/lib/couchrest/helper/streamer.rb +++ b/lib/couchrest/helper/streamer.rb @@ -7,15 +7,22 @@ module CouchRest # Stream a view, yielding one row at a time. Shells out to curl to keep RAM usage low when you have millions of rows. def view name, params = nil, &block - urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}" + urlst = if /^_/.match(name) then + "#{@db.root}/#{name}" + else + name = name.split('/') + dname = name.shift + vname = name.join('/') + "#{@db.root}/_design/#{dname}/_view/#{vname}" + end url = CouchRest.paramify_url urlst, params # puts "stream #{url}" first = nil - IO.popen("curl --silent #{url}") do |view| + IO.popen("curl --silent \"#{url}\"") do |view| first = view.gets # discard header while line = view.gets row = parse_line(line) - block.call row + block.call row unless row.nil? # last line "}]" discarded end end parse_first(first) @@ -41,4 +48,4 @@ module CouchRest end end -end \ No newline at end of file +end diff --git a/lib/couchrest/mixins/attribute_protection.rb b/lib/couchrest/mixins/attribute_protection.rb new file mode 100644 index 0000000..b2efc53 --- /dev/null +++ b/lib/couchrest/mixins/attribute_protection.rb @@ -0,0 +1,74 @@ +module CouchRest + module Mixins + module AttributeProtection + # Attribute protection from mass assignment to CouchRest properties + # + # Protected methods will be removed from + # * new + # * update_attributes + # * upate_attributes_without_saving + # * attributes= + # + # There are two modes of protection + # 1) Declare accessible poperties, assume all the rest are protected + # property :name, :accessible => true + # property :admin # this will be automatically protected + # + # 2) Declare protected properties, assume all the rest are accessible + # property :name # this will not be protected + # property :admin, :protected => true + # + # Note: you cannot set both flags in a single class + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def accessible_properties + properties.select { |prop| prop.options[:accessible] } + end + + def protected_properties + properties.select { |prop| prop.options[:protected] } + end + end + + def accessible_properties + self.class.accessible_properties + end + + def protected_properties + self.class.protected_properties + end + + def remove_protected_attributes(attributes) + protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name } + return attributes if protected_names.empty? + + attributes.reject! do |property_name, property_value| + protected_names.include?(property_name.to_s) + end + + attributes || {} + end + + private + + def properties_to_remove_from_mass_assignment + has_protected = !protected_properties.empty? + has_accessible = !accessible_properties.empty? + + if !has_protected && !has_accessible + [] + elsif has_protected && !has_accessible + protected_properties + elsif has_accessible && !has_protected + properties.reject { |prop| prop.options[:accessible] } + else + raise "Set either :accessible or :protected for #{self.class}, but not both" + end + end + end + end +end diff --git a/lib/couchrest/mixins/class_proxy.rb b/lib/couchrest/mixins/class_proxy.rb index 0101639..5f7bd5b 100644 --- a/lib/couchrest/mixins/class_proxy.rb +++ b/lib/couchrest/mixins/class_proxy.rb @@ -56,7 +56,9 @@ module CouchRest # Mixins::DocumentQueries def all(opts = {}, &block) - @klass.all({:database => @database}.merge(opts), &block) + docs = @klass.all({:database => @database}.merge(opts), &block) + docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs + docs end def count(opts = {}, &block) @@ -64,11 +66,15 @@ module CouchRest end def first(opts = {}) - @klass.first({:database => @database}.merge(opts)) + doc = @klass.first({:database => @database}.merge(opts)) + doc.database = @database if doc && doc.respond_to?(:database) + doc end def get(id) - @klass.get(id, @database) + doc = @klass.get(id, @database) + doc.database = @database if doc && doc.respond_to?(:database) + doc end # Mixins::Views @@ -78,7 +84,9 @@ module CouchRest end def view(name, query={}, &block) - @klass.view(name, {:database => @database}.merge(query), &block) + docs = @klass.view(name, {:database => @database}.merge(query), &block) + docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs + docs end def all_design_doc_versions diff --git a/lib/couchrest/mixins/collection.rb b/lib/couchrest/mixins/collection.rb index b248825..696dae7 100644 --- a/lib/couchrest/mixins/collection.rb +++ b/lib/couchrest/mixins/collection.rb @@ -183,7 +183,7 @@ module CouchRest if @container_class.nil? results else - results['rows'].collect { |row| @container_class.new(row['doc']) } unless results['rows'].nil? + results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil? end end diff --git a/lib/couchrest/mixins/document_queries.rb b/lib/couchrest/mixins/document_queries.rb index d7c8a11..b19442e 100644 --- a/lib/couchrest/mixins/document_queries.rb +++ b/lib/couchrest/mixins/document_queries.rb @@ -51,11 +51,9 @@ module CouchRest # db:: optional option to pass a custom database to use def get(id, db = database) begin - doc = db.get id + get!(id, db) rescue nil - else - new(doc) end end @@ -72,11 +70,11 @@ module CouchRest # db:: optional option to pass a custom database to use def get!(id, db = database) doc = db.get id - new(doc) + create_from_database(doc) end end end end -end \ No newline at end of file +end diff --git a/lib/couchrest/mixins/extended_attachments.rb b/lib/couchrest/mixins/extended_attachments.rb index 8dbac45..5b5e2b8 100644 --- a/lib/couchrest/mixins/extended_attachments.rb +++ b/lib/couchrest/mixins/extended_attachments.rb @@ -14,7 +14,7 @@ module CouchRest # reads the data from an attachment def read_attachment(attachment_name) - Base64.decode64(database.fetch_attachment(self, attachment_name)) + database.fetch_attachment(self, attachment_name) end # modifies a file attachment on the current doc @@ -52,10 +52,6 @@ module CouchRest private - def encode_attachment(data) - ::Base64.encode64(data).gsub(/\r|\n/,'') - end - def get_mime_type(file) ::MIME::Types.type_for(file.path).empty? ? 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/') @@ -65,10 +61,10 @@ module CouchRest content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file]) self['_attachments'][args[:name]] = { 'content_type' => content_type, - 'data' => encode_attachment(args[:file].read) + 'data' => args[:file].read } end end # module ExtendedAttachments end -end \ No newline at end of file +end diff --git a/lib/couchrest/mixins/extended_document_mixins.rb b/lib/couchrest/mixins/extended_document_mixins.rb index 89b25d6..89d650a 100644 --- a/lib/couchrest/mixins/extended_document_mixins.rb +++ b/lib/couchrest/mixins/extended_document_mixins.rb @@ -6,3 +6,4 @@ 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') +require File.join(File.dirname(__FILE__), 'attribute_protection') diff --git a/lib/couchrest/mixins/properties.rb b/lib/couchrest/mixins/properties.rb index d0d8537..9a9c3a4 100644 --- a/lib/couchrest/mixins/properties.rb +++ b/lib/couchrest/mixins/properties.rb @@ -4,7 +4,7 @@ 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})/ + string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/ # $1 = year # $2 = month # $3 = day @@ -197,4 +197,4 @@ module CouchRest end end -end \ No newline at end of file +end diff --git a/lib/couchrest/mixins/views.rb b/lib/couchrest/mixins/views.rb index de530e3..6c95066 100644 --- a/lib/couchrest/mixins/views.rb +++ b/lib/couchrest/mixins/views.rb @@ -137,13 +137,13 @@ module CouchRest 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'] + view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows'] end rescue # fallback for old versions of couchdb that don't # have include_docs support view = fetch_view(db, name, opts, &block) - view['rows'].collect{|r|new(db.get(r['id']))} if view['rows'] + view['rows'].collect{|r|create_from_database(db.get(r['id']))} if view['rows'] end end end @@ -170,4 +170,4 @@ module CouchRest end end -end \ No newline at end of file +end diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 351c344..c6f3c85 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -14,15 +14,18 @@ module CouchRest include CouchRest::Mixins::ExtendedAttachments include CouchRest::Mixins::ClassProxy include CouchRest::Mixins::Collection + include CouchRest::Mixins::AttributeProtection def self.subclasses @subclasses ||= [] end def self.inherited(subklass) + super subklass.send(:include, CouchRest::Mixins::Properties) subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1 def self.inherited(subklass) + super subklass.properties = self.properties.dup end EOS @@ -37,15 +40,20 @@ module CouchRest define_callbacks :save, "result == :halt" define_callbacks :update, "result == :halt" define_callbacks :destroy, "result == :halt" + + # Creates a new instance, bypassing attribute protection + # + # ==== Returns + # a document instance + def self.create_from_database(passed_keys={}) + new(passed_keys, :directly_set_attributes => true) + end - def initialize(passed_keys={}) + def initialize(passed_keys={}, options={}) apply_defaults # defined in CouchRest::Mixins::Properties - passed_keys.each do |k,v| - if self.respond_to?("#{k}=") - self.send("#{k}=", passed_keys.delete(k)) - end - end if passed_keys - super + remove_protected_attributes(passed_keys) unless options[:directly_set_attributes] + directly_set_attributes(passed_keys) unless passed_keys.nil? + super(passed_keys) cast_keys # defined in CouchRest::Mixins::Properties unless self['_id'] && self['_rev'] self['couchrest-type'] = self.class.to_s @@ -150,12 +158,8 @@ module CouchRest # make a copy, we don't want to change arguments attrs = hash.dup %w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)} - attrs.each do |k, v| - raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") - end - attrs.each do |k, v| - self.send("#{k}=",v) - end + check_properties_exist(attrs) + set_attributes(attrs) end alias :attributes= :update_attributes_without_saving @@ -238,7 +242,7 @@ module CouchRest set_unique_id if new? && self.respond_to?(:set_unique_id) result = database.save_doc(self, bulk) mark_as_saved - true + result["ok"] == true end # Saves the document to the db using save. Raises an exception @@ -280,6 +284,26 @@ module CouchRest end end end + + private + + def check_properties_exist(attrs) + attrs.each do |attribute_name, attribute_value| + raise NoMethodError, "#{attribute_name}= method not available, use property :#{attribute_name}" unless self.respond_to?("#{attribute_name}=") + end + end + def directly_set_attributes(hash) + hash.each do |attribute_name, attribute_value| + if self.respond_to?("#{attribute_name}=") + self.send("#{attribute_name}=", hash.delete(attribute_name)) + end + end + end + + def set_attributes(hash) + attrs = remove_protected_attributes(hash) + directly_set_attributes(attrs) + end end end diff --git a/lib/couchrest/more/property.rb b/lib/couchrest/more/property.rb index dccc7dd..efede74 100644 --- a/lib/couchrest/more/property.rb +++ b/lib/couchrest/more/property.rb @@ -32,7 +32,7 @@ module CouchRest @alias = options.delete(:alias) if options[:alias] @default = options.delete(:default) unless options[:default].nil? @casted = options[:casted] ? true : false - @init_method = options[:send] ? options.delete(:send) : 'new' + @init_method = options[:init_method] ? options.delete(:init_method) : 'new' @options = options end diff --git a/lib/couchrest/validation/auto_validate.rb b/lib/couchrest/validation/auto_validate.rb index 4b1b925..d277bb3 100644 --- a/lib/couchrest/validation/auto_validate.rb +++ b/lib/couchrest/validation/auto_validate.rb @@ -65,7 +65,7 @@ module CouchRest # validator to be automatically created on the property # # Integer type - # Using a Integer type causes a validates_is_number + # Using a Integer type causes a validates_numericality_of # validator to be created for the property. integer_only # is set to true # @@ -97,8 +97,7 @@ module CouchRest # presence if opts[:allow_nil] == false - # validates_present property.name, opts - validates_present property.name, options_with_message(opts, property, :presence) + validates_presence_of property.name, options_with_message(opts, property, :presence) end # length @@ -111,8 +110,7 @@ module CouchRest else opts[:maximum] = len end - # validates_length property.name, opts - validates_length property.name, options_with_message(opts, property, :length) + validates_length_of property.name, options_with_message(opts, property, :length) end # format @@ -142,13 +140,11 @@ module CouchRest # numeric validator if "Integer" == property.type opts[:integer_only] = true - # validates_is_number property.name, opts - validates_is_number property.name, options_with_message(opts, property, :is_number) + validates_numericality_of property.name, options_with_message(opts, property, :is_number) elsif Float == property.type opts[:precision] = property.precision opts[:scale] = property.scale - # validates_is_number property.name, opts - validates_is_number property.name, options_with_message(opts, property, :is_number) + validates_numericality_of property.name, options_with_message(opts, property, :is_number) end # marked the property has checked diff --git a/lib/couchrest/validation/validators/confirmation_validator.rb b/lib/couchrest/validation/validators/confirmation_validator.rb index d5fdff1..2a5c587 100644 --- a/lib/couchrest/validation/validators/confirmation_validator.rb +++ b/lib/couchrest/validation/validators/confirmation_validator.rb @@ -81,18 +81,26 @@ module CouchRest # attr_accessor :password_confirmation # attr_accessor :email_repeated # - # validates_is_confirmed :password - # validates_is_confirmed :email, :confirm => :email_repeated + # validates_confirmation_of :password + # validates_confirmation_of :email, :confirm => :email_repeated # # # a call to valid? will return false unless: # # password == password_confirmation # # and # # email == email_repeated # - def validates_is_confirmed(*fields) + def validates_confirmation_of(*fields) opts = opts_from_validator_args(fields) add_validator_to_context(opts, fields, CouchRest::Validation::ConfirmationValidator) end + + def validates_is_confirmed(*fields) + warn "[DEPRECATION] `validates_is_confirmed` is deprecated. Please use `validates_confirmation_of` instead." + validates_confirmation_of(*fields) + end + + + end # module ValidatesIsConfirmed end # module Validation diff --git a/lib/couchrest/validation/validators/format_validator.rb b/lib/couchrest/validation/validators/format_validator.rb index eaeb1e1..fa61212 100644 --- a/lib/couchrest/validation/validators/format_validator.rb +++ b/lib/couchrest/validation/validators/format_validator.rb @@ -99,18 +99,23 @@ module CouchRest # property :email, String # property :zip_code, String # - # validates_format :email, :as => :email_address - # validates_format :zip_code, :with => /^\d{5}$/ + # validates_format_of :email, :as => :email_address + # validates_format_of :zip_code, :with => /^\d{5}$/ # # # a call to valid? will return false unless: # # email is formatted like an email address # # and # # zip_code is a string of 5 digits # - def validates_format(*fields) + def validates_format_of(*fields) opts = opts_from_validator_args(fields) add_validator_to_context(opts, fields, CouchRest::Validation::FormatValidator) end + + def validates_format(*fields) + warn "[DEPRECATION] `validates_format` is deprecated. Please use `validates_format_of` instead." + validates_format_of(*fields) + end end # module ValidatesFormat end # module Validation diff --git a/lib/couchrest/validation/validators/length_validator.rb b/lib/couchrest/validation/validators/length_validator.rb index 272b479..ec80dff 100644 --- a/lib/couchrest/validation/validators/length_validator.rb +++ b/lib/couchrest/validation/validators/length_validator.rb @@ -115,20 +115,25 @@ module CouchRest # property low, Integer # property just_right, Integer # - # validates_length :high, :min => 100000000000 - # validates_length :low, :equals => 0 - # validates_length :just_right, :within => 1..10 + # validates_length_of :high, :min => 100000000000 + # validates_length_of :low, :equals => 0 + # validates_length_of :just_right, :within => 1..10 # # # a call to valid? will return false unless: # # high is greater than or equal to 100000000000 # # low is equal to 0 # # just_right is between 1 and 10 (inclusive of both 1 and 10) # - def validates_length(*fields) + def validates_length_of(*fields) opts = opts_from_validator_args(fields) add_validator_to_context(opts, fields, CouchRest::Validation::LengthValidator) end - + + def validates_length(*fields) + warn "[DEPRECATION] `validates_length` is deprecated. Please use `validates_length_of` instead." + validates_length_of(*fields) + end + end # module ValidatesLength end # module Validation end # module CouchRest diff --git a/lib/couchrest/validation/validators/numeric_validator.rb b/lib/couchrest/validation/validators/numeric_validator.rb index 05269b3..a27e274 100644 --- a/lib/couchrest/validation/validators/numeric_validator.rb +++ b/lib/couchrest/validation/validators/numeric_validator.rb @@ -94,10 +94,15 @@ module CouchRest # Validate whether a field is numeric # - def validates_is_number(*fields) + def validates_numericality_of(*fields) opts = opts_from_validator_args(fields) add_validator_to_context(opts, fields, CouchRest::Validation::NumericValidator) end + + def validates_is_number(*fields) + warn "[DEPRECATION] `validates_is_number` is deprecated. Please use `validates_numericality_of` instead." + validates_numericality_of(*fields) + end end # module ValidatesIsNumber end # module Validation diff --git a/lib/couchrest/validation/validators/required_field_validator.rb b/lib/couchrest/validation/validators/required_field_validator.rb index 17e3132..d8edd81 100644 --- a/lib/couchrest/validation/validators/required_field_validator.rb +++ b/lib/couchrest/validation/validators/required_field_validator.rb @@ -93,16 +93,21 @@ module CouchRest # property :another_required, String # property :yet_again, String # - # validates_present :required_attribute - # validates_present :another_required, :yet_again + # validates_presence_of :required_attribute + # validates_presence_of :another_required, :yet_again # # # a call to valid? will return false unless # # all three attributes are !blank? # end - def validates_present(*fields) + def validates_presence_of(*fields) opts = opts_from_validator_args(fields) add_validator_to_context(opts, fields, CouchRest::Validation::RequiredFieldValidator) end + + def validates_present(*fields) + warn "[DEPRECATION] `validates_present` is deprecated. Please use `validates_presence_of` instead." + validates_presence_of(*fields) + end end # module ValidatesPresent end # module Validation diff --git a/make-gemspec.sh b/make-gemspec.sh deleted file mode 100755 index cd79b64..0000000 --- a/make-gemspec.sh +++ /dev/null @@ -1,2 +0,0 @@ -git clean -fxd -rake gemspec \ No newline at end of file diff --git a/spec/couchrest/core/couchrest_spec.rb b/spec/couchrest/core/couchrest_spec.rb index ef6637c..faf6847 100644 --- a/spec/couchrest/core/couchrest_spec.rb +++ b/spec/couchrest/core/couchrest_spec.rb @@ -54,7 +54,7 @@ describe CouchRest do db[:host].should == "127.0.0.1" end it "should parse a host and db with http" do - db = CouchRest.parse "http://127.0.0.1/my-db" + db = CouchRest.parse "https://127.0.0.1/my-db" db[:database].should == "my-db" db[:host].should == "127.0.0.1" end @@ -68,16 +68,31 @@ describe CouchRest do db[:database].should == "my-db" db[:host].should == "127.0.0.1:5555" end + it "should parse a host with a port and db with https" do + db = CouchRest.parse "https://127.0.0.1:5555/my-db" + db[:database].should == "my-db" + db[:host].should == "127.0.0.1:5555" + end it "should parse just a host" do db = CouchRest.parse "http://127.0.0.1:5555/" db[:database].should be_nil db[:host].should == "127.0.0.1:5555" end + it "should parse just a host with https" do + db = CouchRest.parse "https://127.0.0.1:5555/" + db[:database].should be_nil + db[:host].should == "127.0.0.1:5555" + end it "should parse just a host no slash" do db = CouchRest.parse "http://127.0.0.1:5555" db[:host].should == "127.0.0.1:5555" db[:database].should be_nil end + it "should parse just a host no slash and https" do + db = CouchRest.parse "https://127.0.0.1:5555" + db[:host].should == "127.0.0.1:5555" + db[:database].should be_nil + end it "should get docid" do db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc" db[:database].should == "my-db" @@ -90,7 +105,12 @@ describe CouchRest do db[:host].should == "127.0.0.1:5555" db[:doc].should == "my-doc" end - + it "should get docid with https" do + db = CouchRest.parse "https://127.0.0.1:5555/my-db/my-doc" + db[:database].should == "my-db" + db[:host].should == "127.0.0.1:5555" + db[:doc].should == "my-doc" + end it "should parse a host and db" do db = CouchRest.parse "127.0.0.1/my-db" db[:database].should == "my-db" @@ -101,6 +121,11 @@ describe CouchRest do db[:database].should == "my-db" db[:host].should == "127.0.0.1" end + it "should parse a host and db with https" do + db = CouchRest.parse "https://127.0.0.1/my-db" + db[:database].should == "my-db" + db[:host].should == "127.0.0.1" + end it "should parse a host with a port and db" do db = CouchRest.parse "127.0.0.1:5555/my-db" db[:database].should == "my-db" @@ -111,16 +136,31 @@ describe CouchRest do db[:database].should == "my-db" db[:host].should == "127.0.0.1:5555" end + it "should parse a host with a port and db with https" do + db = CouchRest.parse "http://127.0.0.1:5555/my-db" + db[:database].should == "my-db" + db[:host].should == "127.0.0.1:5555" + end it "should parse just a host" do db = CouchRest.parse "http://127.0.0.1:5555/" db[:database].should be_nil db[:host].should == "127.0.0.1:5555" end + it "should parse just a host with https" do + db = CouchRest.parse "https://127.0.0.1:5555/" + db[:database].should be_nil + db[:host].should == "127.0.0.1:5555" + end it "should parse just a host no slash" do db = CouchRest.parse "http://127.0.0.1:5555" db[:host].should == "127.0.0.1:5555" db[:database].should be_nil end + it "should parse just a host no slash and https" do + db = CouchRest.parse "https://127.0.0.1:5555" + db[:host].should == "127.0.0.1:5555" + db[:database].should be_nil + end it "should get docid" do db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc" db[:database].should == "my-db" @@ -133,6 +173,12 @@ describe CouchRest do db[:host].should == "127.0.0.1:5555" db[:doc].should == "my-doc" end + it "should get docid with https" do + db = CouchRest.parse "https://127.0.0.1:5555/my-db/my-doc" + db[:database].should == "my-db" + db[:host].should == "127.0.0.1:5555" + db[:doc].should == "my-doc" + end end describe "easy initializing a database adapter" do diff --git a/spec/couchrest/core/database_spec.rb b/spec/couchrest/core/database_spec.rb index ff7982f..28a57dd 100644 --- a/spec/couchrest/core/database_spec.rb +++ b/spec/couchrest/core/database_spec.rb @@ -65,22 +65,27 @@ describe CouchRest::Database do describe "saving a view" do before(:each) do - @view = {'test' => {'map' => 'function(doc) { - if (doc.word && !/\W/.test(doc.word)) { - emit(doc.word,null); + @view = {'test' => {'map' => <<-JS + function(doc) { + var reg = new RegExp("\\\\W"); + if (doc.word && !reg.test(doc.word)) { + emit(doc.word,null); + } } - }'}} + JS + }} @db.save_doc({ "_id" => "_design/test", :views => @view }) end it "should work properly" do - @db.bulk_save([ + r = @db.bulk_save([ {"word" => "once"}, {"word" => "and again"} ]) - @db.view('test/test')['total_rows'].should == 1 + r = @db.view('test/test') + r['total_rows'].should == 1 end it "should round trip" do @db.get("_design/test")['views'].should == @view @@ -131,9 +136,16 @@ describe CouchRest::Database do rs = @db.view('first/test', :include_docs => true) do |row| rows << row end - rows.length.should == 4 + rows.length.should == 3 rs["total_rows"].should == 3 end + it "should accept a block with several params" do + rows = [] + rs = @db.view('first/test', :include_docs => true, :limit => 2) do |row| + rows << row + end + rows.length.should == 2 + end end describe "GET (document by id) when the doc exists" do @@ -539,6 +551,53 @@ describe CouchRest::Database do end + describe "UPDATE existing document" do + before :each do + @id = @db.save_doc({ + 'article' => 'Pete Doherty Kicked Out For Nazi Anthem', + 'upvotes' => 10, + 'link' => 'http://beatcrave.com/2009-11-30/pete-doherty-kicked-out-for-nazi-anthem/'})['id'] + end + it "should work under normal conditions" do + @db.update_doc @id do |doc| + doc['upvotes'] += 1 + doc + end + @db.get(@id)['upvotes'].should == 11 + end + it "should fail if update_limit is reached" do + lambda do + @db.update_doc @id do |doc| + # modify and save the doc so that a collision happens + conflicting_doc = @db.get @id + conflicting_doc['upvotes'] += 1 + @db.save_doc conflicting_doc + + # then try saving it through the update + doc['upvotes'] += 1 + doc + end + end.should raise_error(RestClient::RequestFailed) + end + it "should not fail if update_limit is not reached" do + limit = 5 + lambda do + @db.update_doc @id do |doc| + # same as the last spec except we're only forcing 5 conflicts + if limit > 0 + conflicting_doc = @db.get @id + conflicting_doc['upvotes'] += 1 + @db.save_doc conflicting_doc + limit -= 1 + end + doc['upvotes'] += 1 + doc + end + end.should_not raise_error + @db.get(@id)['upvotes'].should == 16 + end + end + describe "COPY existing document" do before :each do @r = @db.save_doc({'artist' => 'Zappa', 'title' => 'Muffin Man'}) @@ -704,11 +763,11 @@ describe CouchRest::Database do it "should recreate a db even tho it doesn't exist" do @cr.databases.should_not include(@db2.name) - begin @db2.recreate! rescue nil end + @db2.recreate! @cr.databases.should include(@db2.name) end end -end \ No newline at end of file +end diff --git a/spec/couchrest/helpers/streamer_spec.rb b/spec/couchrest/helpers/streamer_spec.rb index cf828ca..48ec6c3 100644 --- a/spec/couchrest/helpers/streamer_spec.rb +++ b/spec/couchrest/helpers/streamer_spec.rb @@ -9,6 +9,14 @@ describe CouchRest::Streamer do @streamer = CouchRest::Streamer.new(@db) @docs = (1..1000).collect{|i| {:integer => i, :string => i.to_s}} @db.bulk_save(@docs) + @db.save_doc({ + "_id" => "_design/first", + :views => { + :test => { + :map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}" + } + } + }) end it "should yield each row in a view" do @@ -19,5 +27,26 @@ describe CouchRest::Streamer do end count.should == 1001 end - -end \ No newline at end of file + + it "should accept several params" do + count = 0 + @streamer.view("_design/first/_view/test", :include_docs => true, :limit => 5) do |row| + count += 1 + end + count.should == 5 + end + + it "should accept both view formats" do + count = 0 + @streamer.view("_design/first/_view/test") do |row| + count += 1 + end + count.should == 2000 + count = 0 + @streamer.view("first/test") do |row| + count += 1 + end + count.should == 2000 + end + +end diff --git a/spec/couchrest/more/attribute_protection_spec.rb b/spec/couchrest/more/attribute_protection_spec.rb new file mode 100644 index 0000000..f773167 --- /dev/null +++ b/spec/couchrest/more/attribute_protection_spec.rb @@ -0,0 +1,150 @@ +require File.expand_path("../../../spec_helper", __FILE__) + +describe "ExtendedDocument", "no declarations" do + class NoProtection < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :name + property :phone + end + + it "should not protect anything through new" do + user = NoProtection.new(:name => "will", :phone => "555-5555") + + user.name.should == "will" + user.phone.should == "555-5555" + end + + it "should not protect anything through attributes=" do + user = NoProtection.new + user.attributes = {:name => "will", :phone => "555-5555"} + + user.name.should == "will" + user.phone.should == "555-5555" + end + + it "should recreate from the database properly" do + user = NoProtection.new + user.name = "will" + user.phone = "555-5555" + user.save! + + user = NoProtection.get(user.id) + user.name.should == "will" + user.phone.should == "555-5555" + end +end + +describe "ExtendedDocument", "accessible flag" do + class WithAccessible < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :name, :accessible => true + property :admin, :default => false + end + + it "should recognize accessible properties" do + props = WithAccessible.accessible_properties.map { |prop| prop.name} + props.should include("name") + props.should_not include("admin") + end + + it "should protect non-accessible properties set through new" do + user = WithAccessible.new(:name => "will", :admin => true) + + user.name.should == "will" + user.admin.should == false + end + + it "should protect non-accessible properties set through attributes=" do + user = WithAccessible.new + user.attributes = {:name => "will", :admin => true} + + user.name.should == "will" + user.admin.should == false + end +end + +describe "ExtendedDocument", "protected flag" do + class WithProtected < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :name + property :admin, :default => false, :protected => true + end + + it "should recognize protected properties" do + props = WithProtected.protected_properties.map { |prop| prop.name} + props.should_not include("name") + props.should include("admin") + end + + it "should protect non-accessible properties set through new" do + user = WithProtected.new(:name => "will", :admin => true) + + user.name.should == "will" + user.admin.should == false + end + + it "should protect non-accessible properties set through attributes=" do + user = WithProtected.new + user.attributes = {:name => "will", :admin => true} + + user.name.should == "will" + user.admin.should == false + end +end + +describe "ExtendedDocument", "protected flag" do + class WithBoth < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :name, :accessible => true + property :admin, :default => false, :protected => true + end + + it "should raise an error when both are set" do + lambda { WithBoth.new }.should raise_error + end +end + +describe "ExtendedDocument", "from database" do + class WithProtected < CouchRest::ExtendedDocument + use_database TEST_SERVER.default_database + property :name + property :admin, :default => false, :protected => true + view_by :name + end + + before(:each) do + @user = WithProtected.new + @user.name = "will" + @user.admin = true + @user.save! + end + + def verify_attrs(user) + user.name.should == "will" + user.admin.should == true + end + + it "ExtendedDocument#get should not strip protected attributes" do + reloaded = WithProtected.get( @user.id ) + verify_attrs reloaded + end + + it "ExtendedDocument#get! should not strip protected attributes" do + reloaded = WithProtected.get!( @user.id ) + verify_attrs reloaded + end + + it "ExtendedDocument#all should not strip protected attributes" do + # all creates a CollectionProxy + docs = WithProtected.all(:key => @user.id) + docs.size.should == 1 + reloaded = docs.first + verify_attrs reloaded + end + + it "views should not strip protected attributes" do + docs = WithProtected.by_name(:startkey => "will", :endkey => "will") + reloaded = docs.first + verify_attrs reloaded + end +end diff --git a/spec/couchrest/more/casted_extended_doc_spec.rb b/spec/couchrest/more/casted_extended_doc_spec.rb index 4df60c1..e92d34f 100644 --- a/spec/couchrest/more/casted_extended_doc_spec.rb +++ b/spec/couchrest/more/casted_extended_doc_spec.rb @@ -49,7 +49,7 @@ describe "assigning a value to casted attribute after initializing an object" do end it "should cast attribute" do - @car.driver = JSON.parse(JSON.generate(@driver)) + @car.driver = JSON.parse(@driver.to_json) @car.driver.should be_instance_of(Driver) end @@ -60,7 +60,7 @@ describe "casting an extended document from parsed JSON" do before(:each) do @driver = Driver.new(:name => 'Matt') @car = Car.new(:name => 'Renault 306', :driver => @driver) - @new_car = Car.new(JSON.parse(JSON.generate(@car))) + @new_car = Car.new(JSON.parse(@car.to_json)) end it "should cast casted attribute" do diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 04ab737..a2a8a96 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -224,7 +224,7 @@ describe CouchRest::CastedModel do it "should not fail if the casted model doesn't have validation" do Cat.property :masters, :cast_as => ['Person'], :default => [] - Cat.validates_present :name + Cat.validates_presence_of :name cat = Cat.new(:name => 'kitty') cat.should be_valid cat.masters.push Person.new diff --git a/spec/couchrest/more/extended_doc_inherited_spec.rb b/spec/couchrest/more/extended_doc_inherited_spec.rb new file mode 100644 index 0000000..d52fbd9 --- /dev/null +++ b/spec/couchrest/more/extended_doc_inherited_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path('../../../spec_helper', __FILE__) + +begin + require 'rubygems' unless ENV['SKIP_RUBYGEMS'] + require 'activesupport' + ActiveSupport::JSON.backend = :JSONGem + + class PlainParent + class_inheritable_accessor :foo + self.foo = :bar + end + + class PlainChild < PlainParent + end + + class ExtendedParent < CouchRest::ExtendedDocument + class_inheritable_accessor :foo + self.foo = :bar + end + + class ExtendedChild < ExtendedParent + end + + describe "Using chained inheritance without CouchRest::ExtendedDocument" do + it "should preserve inheritable attributes" do + PlainParent.foo.should == :bar + PlainChild.foo.should == :bar + end + end + + describe "Using chained inheritance with CouchRest::ExtendedDocument" do + it "should preserve inheritable attributes" do + ExtendedParent.foo.should == :bar + ExtendedChild.foo.should == :bar + end + end + +rescue LoadError + puts "This spec requires 'active_support' to be loaded" +end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 75124b4..f055576 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require File.expand_path("../../../spec_helper", __FILE__) require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'course') @@ -127,6 +129,11 @@ describe "ExtendedDocument" do @obj.should be_new_document @obj.should be_new_record end + + it "should not failed on a nil value in argument" do + @obj = Basic.new(nil) + @obj.should == { 'couchrest-type' => 'Basic' } + end end describe "creating a new document" do @@ -728,7 +735,7 @@ describe "ExtendedDocument" do it "should not fail if the nested casted model doesn't have validation" do Cat.property :trainer, :cast_as => 'Person' - Cat.validates_present :name + Cat.validates_presence_of :name cat = Cat.new(:name => 'Mr Bigglesworth') cat.trainer = Person.new cat.trainer.validatable?.should be_false diff --git a/spec/couchrest/more/extended_doc_view_spec.rb b/spec/couchrest/more/extended_doc_view_spec.rb index de14f63..c903cf5 100644 --- a/spec/couchrest/more/extended_doc_view_spec.rb +++ b/spec/couchrest/more/extended_doc_view_spec.rb @@ -25,39 +25,33 @@ describe "ExtendedDocument views" do written_at += 24 * 3600 end end - it "should have a design doc" do Article.design_doc["views"]["by_date"].should_not be_nil end - it "should save the design doc" do Article.by_date #rescue nil doc = Article.database.get Article.design_doc.id doc['views']['by_date'].should_not be_nil end - it "should return the matching raw view result" do view = Article.by_date :raw => true view['rows'].length.should == 4 end - it "should not include non-Articles" do Article.database.save_doc({"date" => 1}) view = Article.by_date :raw => true view['rows'].length.should == 4 end - it "should return the matching objects (with default argument :descending => true)" do articles = Article.by_date articles.collect{|a|a.title}.should == @titles.reverse end - it "should allow you to override default args" do articles = Article.by_date :descending => false articles.collect{|a|a.title}.should == @titles end end - + describe "another model with a simple view" do before(:all) do reset_test_db! @@ -96,8 +90,7 @@ describe "ExtendedDocument views" do courses[0]["doc"]["title"].should =='aaa' end end - - + describe "a ducktype view" do before(:all) do reset_test_db! @@ -117,7 +110,7 @@ describe "ExtendedDocument views" do @as[0]['_id'].should == @id end end - + describe "a model class not tied to a database" do before(:all) do reset_test_db! @@ -198,7 +191,7 @@ describe "ExtendedDocument views" do Unattached.model_design_doc(@db)['_rev'].should_not == original_revision end end - + describe "class proxy" do before(:all) do reset_test_db! @@ -254,6 +247,36 @@ describe "ExtendedDocument views" do u = @us.first u.title.should =~ /\A...\z/ end + it "should set database on first retreived document" do + u = @us.first + u.database.should === DB + end + it "should set database on all retreived documents" do + @us.all.each do |u| + u.database.should === DB + end + end + it "should set database on each retreived document" do + rs = @us.by_title :startkey=>"bbb", :endkey=>"eee" + rs.length.should == 3 + rs.each do |u| + u.database.should === DB + end + end + it "should set database on document retreived by id" do + u = @us.get(@first_id) + u.database.should === DB + end + it "should not attempt to set database on raw results using :all" do + @us.all(:raw => true).each do |u| + u.respond_to?(:database).should be_false + end + end + it "should not attempt to set database on raw results using view" do + @us.by_title(:raw => true).each do |u| + u.respond_to?(:database).should be_false + end + end it "should clean up design docs left around on specific database" do @us.by_title original_id = @us.model_design_doc['_rev'] @@ -262,7 +285,7 @@ describe "ExtendedDocument views" do @us.model_design_doc['_rev'].should_not == original_id end end - + describe "a model with a compound key view" do before(:all) do Article.by_user_id_and_date.each{|a| a.destroy(true)} @@ -295,7 +318,7 @@ describe "ExtendedDocument views" do articles[0].title.should == "even more interesting" end end - + describe "with a custom view" do before(:all) do @titles = ["very uniq one", "even less interesting", "some fun", @@ -311,18 +334,18 @@ describe "ExtendedDocument views" do view = Article.by_tags :raw => true view['rows'].length.should == 5 end - + it "should be default to :reduce => false" do ars = Article.by_tags ars.first.tags.first.should == 'cool' end - + it "should be raw when reduce is true" do view = Article.by_tags :reduce => true, :group => true view['rows'].find{|r|r['key'] == 'cool'}['value'].should == 3 end end - + # TODO: moved to Design, delete describe "adding a view" do before(:each) do @@ -344,7 +367,7 @@ 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! @@ -354,7 +377,7 @@ describe "ExtendedDocument views" do a = Article.new(:title => title, :date => Date.today) a.save end - + titles = ["yesterday very uniq one", "yesterday really interesting", "yesterday some fun", "yesterday really awesome", "yesterday crazy bob", "yesterday this rocks"] titles.each_with_index do |title,i| @@ -390,11 +413,11 @@ describe "ExtendedDocument views" 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 @@ -408,10 +431,6 @@ describe "ExtendedDocument views" do 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 diff --git a/spec/couchrest/more/property_spec.rb b/spec/couchrest/more/property_spec.rb index 8df2867..0e25916 100644 --- a/spec/couchrest/more/property_spec.rb +++ b/spec/couchrest/more/property_spec.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require File.expand_path('../../../spec_helper', __FILE__) require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'card') @@ -5,6 +6,7 @@ require File.join(FIXTURE_PATH, 'more', 'invoice') require File.join(FIXTURE_PATH, 'more', 'service') require File.join(FIXTURE_PATH, 'more', 'event') require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'user') describe "ExtendedDocument properties" do @@ -55,6 +57,30 @@ describe "ExtendedDocument properties" do @card.updated_at.should_not be_nil end + + describe "mass assignment protection" do + + it "should not store protected attribute using mass assignment" do + cat_toy = CatToy.new(:name => "Zorro") + cat = Cat.create(:name => "Helena", :toys => [cat_toy], :favorite_toy => cat_toy, :number => 1) + cat.number.should be_nil + cat.number = 1 + cat.save + cat.number.should == 1 + end + + it "should not store protected attribute when 'declare accessible poperties, assume all the rest are protected'" do + user = User.create(:name => "Marcos Tapajós", :admin => true) + user.admin.should be_nil + end + + it "should not store protected attribute when 'declare protected properties, assume all the rest are accessible'" do + user = SpecialUser.create(:name => "Marcos Tapajós", :admin => true) + user.admin.should be_nil + end + + end + describe "validation" do before(:each) do @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA") diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index 6f4bb7a..dbc9e8c 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -2,6 +2,7 @@ class Article < CouchRest::ExtendedDocument use_database DB unique_id :slug + provides_collection :article_details, 'Article', 'by_date', :descending => true, :include_docs => true view_by :date, :descending => true view_by :user_id, :date @@ -31,4 +32,4 @@ class Article < CouchRest::ExtendedDocument def generate_slug_from_title self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new? end -end \ No newline at end of file +end diff --git a/spec/fixtures/more/card.rb b/spec/fixtures/more/card.rb index 484ba23..7365166 100644 --- a/spec/fixtures/more/card.rb +++ b/spec/fixtures/more/card.rb @@ -17,6 +17,6 @@ class Card < CouchRest::ExtendedDocument timestamps! # Validation - validates_present :first_name + validates_presence_of :first_name end \ No newline at end of file diff --git a/spec/fixtures/more/cat.rb b/spec/fixtures/more/cat.rb index a3cb054..68fcb43 100644 --- a/spec/fixtures/more/cat.rb +++ b/spec/fixtures/more/cat.rb @@ -4,9 +4,10 @@ class Cat < CouchRest::ExtendedDocument # Set the default database to use use_database DB - property :name - property :toys, :cast_as => ['CatToy'], :default => [] - property :favorite_toy, :cast_as => 'CatToy' + property :name, :accessible => true + property :toys, :cast_as => ['CatToy'], :default => [], :accessible => true + property :favorite_toy, :cast_as => 'CatToy', :accessible => true + property :number end class CatToy < Hash @@ -15,5 +16,5 @@ class CatToy < Hash property :name - validates_present :name + validates_presence_of :name end \ No newline at end of file diff --git a/spec/fixtures/more/event.rb b/spec/fixtures/more/event.rb index 988c5cd..97aa248 100644 --- a/spec/fixtures/more/event.rb +++ b/spec/fixtures/more/event.rb @@ -2,8 +2,8 @@ class Event < CouchRest::ExtendedDocument use_database DB property :subject - property :occurs_at, :cast_as => 'Time', :send => 'parse' - property :end_date, :cast_as => 'Date', :send => 'parse' + property :occurs_at, :cast_as => 'Time', :init_method => 'parse' + property :end_date, :cast_as => 'Date', :init_method => 'parse' end \ No newline at end of file diff --git a/spec/fixtures/more/invoice.rb b/spec/fixtures/more/invoice.rb index 273e6f2..0666e5c 100644 --- a/spec/fixtures/more/invoice.rb +++ b/spec/fixtures/more/invoice.rb @@ -11,7 +11,7 @@ class Invoice < CouchRest::ExtendedDocument property :location # Validation - validates_present :client_name, :employee_name - validates_present :location, :message => "Hey stupid!, you forgot the location" + validates_presence_of :client_name, :employee_name + validates_presence_of :location, :message => "Hey stupid!, you forgot the location" end \ No newline at end of file diff --git a/spec/fixtures/more/user.rb b/spec/fixtures/more/user.rb new file mode 100644 index 0000000..f9bbf97 --- /dev/null +++ b/spec/fixtures/more/user.rb @@ -0,0 +1,22 @@ +class User < CouchRest::ExtendedDocument + # Set the default database to use + use_database DB + property :name, :accessible => true + property :admin # this will be automatically protected +end + +class SpecialUser < CouchRest::ExtendedDocument + # Set the default database to use + use_database DB + property :name # this will not be protected + property :admin, :protected => true +end + +# There are two modes of protection +# 1) Declare accessible poperties, assume all the rest are protected +# property :name, :accessible => true +# property :admin # this will be automatically protected +# +# 2) Declare protected properties, assume all the rest are accessible +# property :name # this will not be protected +# property :admin, :protected => true