Merge branch 'master' of git://github.com/couchrest/couchrest

This commit is contained in:
Sam Lown 2010-03-03 00:17:54 +00:00
commit 857695e219
44 changed files with 798 additions and 371 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
.DS_Store
html/*
pkg
*.swp
couchrest.gemspec

152
README.md
View file

@ -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 Herokus 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 <http://jchris.lighthouseapp.com/projects/17807-couchrest/overview>.
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

View file

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

View file

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

View file

@ -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<rest-client>, [">= 0.5"])
s.add_runtime_dependency(%q<mime-types>, [">= 1.15"])
else
s.add_dependency(%q<rest-client>, [">= 0.5"])
s.add_dependency(%q<mime-types>, [">= 1.15"])
end
else
s.add_dependency(%q<rest-client>, [">= 0.5"])
s.add_dependency(%q<mime-types>, [">= 1.15"])
end
end

View file

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

View file

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

View file

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

View file

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

View file

@ -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
@ -139,13 +138,25 @@ module CouchRest
#
# If <tt>bulk</tt> 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 <tt>batch</tt> 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

View file

@ -7,15 +7,22 @@ module CouchRest
# Stream a view, yielding one row at a time. Shells out to <tt>curl</tt> 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)

View file

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

View file

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

View file

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

View file

@ -51,11 +51,9 @@ module CouchRest
# db<Database>:: 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,7 +70,7 @@ module CouchRest
# db<Database>:: 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

View file

@ -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,7 +61,7 @@ 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

View file

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

View file

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

View file

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

View file

@ -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
@ -38,14 +41,19 @@ module CouchRest
define_callbacks :update, "result == :halt"
define_callbacks :destroy, "result == :halt"
def initialize(passed_keys={})
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))
# 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
end if passed_keys
super
def initialize(passed_keys={}, options={})
apply_defaults # defined in CouchRest::Mixins::Properties
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
@ -281,5 +285,25 @@ module CouchRest
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

View file

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

View file

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

View file

@ -81,19 +81,27 @@ 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
end # module CouchRest

View file

@ -99,19 +99,24 @@ 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
end # module CouchRest

View file

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

View file

@ -94,11 +94,16 @@ 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
end # module CouchRest

View file

@ -93,17 +93,22 @@ 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
end # module CouchRest

View file

@ -1,2 +0,0 @@
git clean -fxd
rake gemspec

View file

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

View file

@ -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)) {
@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,7 +763,7 @@ 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

View file

@ -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
@ -20,4 +28,25 @@ describe CouchRest::Streamer do
count.should == 1001
end
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,33 +25,27 @@ 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
@ -97,7 +91,6 @@ describe "ExtendedDocument views" do
end
end
describe "a ducktype view" 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']
@ -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

View file

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

View file

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

View file

@ -17,6 +17,6 @@ class Card < CouchRest::ExtendedDocument
timestamps!
# Validation
validates_present :first_name
validates_presence_of :first_name
end

View file

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

View file

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

View file

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

22
spec/fixtures/more/user.rb vendored Normal file
View file

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