Merge commit 'mattetti/master'

This commit is contained in:
Peter Gumeson 2009-08-12 16:48:13 -07:00
commit 1938270d65
30 changed files with 536 additions and 106 deletions

View file

@ -17,7 +17,7 @@ Note: CouchRest only support CouchDB 0.9.0 or newer.
Alternatively, you can install from Github: Alternatively, you can install from Github:
$ gem sources -a http://gems.github.com (you only have to do this once) $ gem sources -a http://gems.github.com (you only have to do this once)
$ sudo gem install mattetti-couchrest $ sudo gem install couchrest-couchrest
### Relax, it's RESTful ### Relax, it's RESTful

View file

@ -2,7 +2,7 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{couchrest} s.name = %q{couchrest}
s.version = "0.31" s.version = "0.33"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti"] s.authors = ["J. Chris Anderson", "Matt Aimonetti"]
@ -10,7 +10,7 @@ Gem::Specification.new do |s|
s.description = %q{CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments.} s.description = %q{CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments.}
s.email = %q{jchris@apache.org} s.email = %q{jchris@apache.org}
s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"] s.extra_rdoc_files = ["README.md", "LICENSE", "THANKS.md"]
s.files = ["LICENSE", "README.md", "Rakefile", "THANKS.md", "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/server.rb", "lib/couchrest/core/view.rb", "lib/couchrest/helper", "lib/couchrest/helper/pager.rb", "lib/couchrest/helper/streamer.rb", "lib/couchrest/helper/upgrade.rb", "lib/couchrest/mixins", "lib/couchrest/mixins/attachments.rb", "lib/couchrest/mixins/callbacks.rb", "lib/couchrest/mixins/class_proxy.rb", "lib/couchrest/mixins/collection.rb", "lib/couchrest/mixins/design_doc.rb", "lib/couchrest/mixins/document_queries.rb", "lib/couchrest/mixins/extended_attachments.rb", "lib/couchrest/mixins/extended_document_mixins.rb", "lib/couchrest/mixins/properties.rb", "lib/couchrest/mixins/validation.rb", "lib/couchrest/mixins/views.rb", "lib/couchrest/mixins.rb", "lib/couchrest/monkeypatches.rb", "lib/couchrest/more", "lib/couchrest/more/casted_model.rb", "lib/couchrest/more/extended_document.rb", "lib/couchrest/more/property.rb", "lib/couchrest/support", "lib/couchrest/support/blank.rb", "lib/couchrest/support/class.rb", "lib/couchrest/support/rails.rb", "lib/couchrest/validation", "lib/couchrest/validation/auto_validate.rb", "lib/couchrest/validation/contextual_validators.rb", "lib/couchrest/validation/validation_errors.rb", "lib/couchrest/validation/validators", "lib/couchrest/validation/validators/absent_field_validator.rb", "lib/couchrest/validation/validators/confirmation_validator.rb", "lib/couchrest/validation/validators/format_validator.rb", "lib/couchrest/validation/validators/formats", "lib/couchrest/validation/validators/formats/email.rb", "lib/couchrest/validation/validators/formats/url.rb", "lib/couchrest/validation/validators/generic_validator.rb", "lib/couchrest/validation/validators/length_validator.rb", "lib/couchrest/validation/validators/method_validator.rb", "lib/couchrest/validation/validators/numeric_validator.rb", "lib/couchrest/validation/validators/required_field_validator.rb", "lib/couchrest.rb", "spec/couchrest", "spec/couchrest/core", "spec/couchrest/core/couchrest_spec.rb", "spec/couchrest/core/database_spec.rb", "spec/couchrest/core/design_spec.rb", "spec/couchrest/core/document_spec.rb", "spec/couchrest/core/server_spec.rb", "spec/couchrest/helpers", "spec/couchrest/helpers/pager_spec.rb", "spec/couchrest/helpers/streamer_spec.rb", "spec/couchrest/more", "spec/couchrest/more/casted_extended_doc_spec.rb", "spec/couchrest/more/casted_model_spec.rb", "spec/couchrest/more/extended_doc_attachment_spec.rb", "spec/couchrest/more/extended_doc_spec.rb", "spec/couchrest/more/extended_doc_subclass_spec.rb", "spec/couchrest/more/extended_doc_view_spec.rb", "spec/couchrest/more/property_spec.rb", "spec/fixtures", "spec/fixtures/attachments", "spec/fixtures/attachments/couchdb.png", "spec/fixtures/attachments/README", "spec/fixtures/attachments/test.html", "spec/fixtures/more", "spec/fixtures/more/article.rb", "spec/fixtures/more/card.rb", "spec/fixtures/more/cat.rb", "spec/fixtures/more/course.rb", "spec/fixtures/more/event.rb", "spec/fixtures/more/invoice.rb", "spec/fixtures/more/person.rb", "spec/fixtures/more/question.rb", "spec/fixtures/more/service.rb", "spec/fixtures/views", "spec/fixtures/views/lib.js", "spec/fixtures/views/test_view", "spec/fixtures/views/test_view/lib.js", "spec/fixtures/views/test_view/only-map.js", "spec/fixtures/views/test_view/test-map.js", "spec/fixtures/views/test_view/test-reduce.js", "spec/spec.opts", "spec/spec_helper.rb", "utils/remap.rb", "utils/subset.rb"] s.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.homepage = %q{http://github.com/jchris/couchrest} s.homepage = %q{http://github.com/jchris/couchrest}
s.require_paths = ["lib"] s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.4} s.rubygems_version = %q{1.3.4}

View file

@ -1,18 +1,31 @@
== 0.40 == 0.33
=== Notes
This release slightly modifies the API and if you were using a previous version of CouchRest,
you will need to make the following modifications.
* Major enhancements * Major enhancements
* * Added a new Rack logger middleware letting you log/save requests/queries (Matt Aimonetti)
* Minor enhancements
* Added #amount_pages to a paginated result array (Matt Aimonetti)
* Ruby 1.9.2 compatible (Matt Aimonetti)
* Added a property? method for property cast as :boolean (John Wood)
* Added an option to force the deletion of a attachments (bypass 409s) (Matt Aimonetti)
* Created a new abstraction layer for the REST API (Matt Aimonetti)
* Bug fix: made ExtendedDocument#all compatible with Couch 0.10 (tc)
== 0.32
* Major enhancements
* ExtendedDocument.get doesn't raise an exception anymore. If no documents are found nil is returned.
* ExtendedDocument.get! works the say #get used to work and will raise an exception if a document isn't found.
* Minor enhancements * Minor enhancements
* Bug fix: Model.all(:keys => [1,2]) was not working (Matt Aimonetti)
* Added ValidationErrors#count in order to play nicely with Rails (Peter Wagenet)
* Bug fix: class proxy design doc refresh (Daniel Kirsh)
* Bug fix: the count method on the proxy collection was missing (Daniel Kirsch)
* Added #amount_pages to a paginated collection. (Matt Aimonetti) * Added #amount_pages to a paginated collection. (Matt Aimonetti)
== 0.31 == 0.31
@ -47,4 +60,4 @@ you will need to make the following modifications.
--- ---
Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest. Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
You can see the full commit history on GitHub: http://github.com/mattetti/couchrest/commits/master/ You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/

View file

@ -28,7 +28,7 @@ require 'couchrest/monkeypatches'
# = CouchDB, close to the metal # = CouchDB, close to the metal
module CouchRest module CouchRest
VERSION = '0.31' unless self.const_defined?("VERSION") VERSION = '0.33' unless self.const_defined?("VERSION")
autoload :Server, 'couchrest/core/server' autoload :Server, 'couchrest/core/server'
autoload :Database, 'couchrest/core/database' autoload :Database, 'couchrest/core/database'
@ -45,9 +45,14 @@ module CouchRest
autoload :ExtendedDocument, 'couchrest/more/extended_document' autoload :ExtendedDocument, 'couchrest/more/extended_document'
autoload :CastedModel, 'couchrest/more/casted_model' autoload :CastedModel, 'couchrest/more/casted_model'
require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'rest_api')
require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction') require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction')
require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') require File.join(File.dirname(__FILE__), 'couchrest', 'mixins')
require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails) require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails)
# we extend CouchRest with the RestAPI module which gives us acess to
# the get, post, put, delete and copy
CouchRest.extend(::RestAPI)
# The CouchRest module methods handle the basic JSON serialization # The CouchRest module methods handle the basic JSON serialization
# and deserialization, as well as query parameters. The module also includes # and deserialization, as well as query parameters. The module also includes
@ -140,52 +145,6 @@ module CouchRest
cr.database(parsed[:database]) cr.database(parsed[:database])
end end
def put(uri, doc = nil)
payload = doc.to_json if doc
begin
JSON.parse(HttpAbstraction.put(uri, payload))
rescue Exception => e
if $DEBUG
raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}"
else
raise e
end
end
end
def get(uri)
begin
JSON.parse(HttpAbstraction.get(uri), :max_nesting => false)
rescue => e
if $DEBUG
raise "Error while sending a GET request #{uri}\n: #{e}"
else
raise e
end
end
end
def post uri, doc = nil
payload = doc.to_json if doc
begin
JSON.parse(HttpAbstraction.post(uri, payload))
rescue Exception => e
if $DEBUG
raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}"
else
raise e
end
end
end
def delete uri
JSON.parse(HttpAbstraction.delete(uri))
end
def copy uri, destination
JSON.parse(HttpAbstraction.copy(uri, {'Destination' => destination}))
end
def paramify_url url, params = {} def paramify_url url, params = {}
if params && !params.empty? if params && !params.empty?
query = params.collect do |k,v| query = params.collect do |k,v|

View file

@ -113,10 +113,21 @@ module CouchRest
end end
# DELETE an attachment directly from CouchDB # DELETE an attachment directly from CouchDB
def delete_attachment doc, name def delete_attachment(doc, name, force=false)
uri = url_for_attachment(doc, name) uri = url_for_attachment(doc, name)
# this needs a rev # this needs a rev
JSON.parse(HttpAbstraction.delete(uri)) begin
JSON.parse(HttpAbstraction.delete(uri))
rescue Exception => error
if force
# get over a 409
doc = get(doc['_id'])
uri = url_for_attachment(doc, name)
JSON.parse(HttpAbstraction.delete(uri))
else
error
end
end
end end
# Save a document to CouchDB. This will use the <tt>_id</tt> field from # Save a document to CouchDB. This will use the <tt>_id</tt> field from

View file

@ -0,0 +1,49 @@
module RestAPI
def put(uri, doc = nil)
payload = doc.to_json if doc
begin
JSON.parse(HttpAbstraction.put(uri, payload))
rescue Exception => e
if $DEBUG
raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}"
else
raise e
end
end
end
def get(uri)
begin
JSON.parse(HttpAbstraction.get(uri), :max_nesting => false)
rescue => e
if $DEBUG
raise "Error while sending a GET request #{uri}\n: #{e}"
else
raise e
end
end
end
def post(uri, doc = nil)
payload = doc.to_json if doc
begin
JSON.parse(HttpAbstraction.post(uri, payload))
rescue Exception => e
if $DEBUG
raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}"
else
raise e
end
end
end
def delete(uri)
JSON.parse(HttpAbstraction.delete(uri))
end
def copy(uri, destination)
JSON.parse(HttpAbstraction.copy(uri, {'Destination' => destination}))
end
end

View file

@ -0,0 +1,263 @@
####################################
# USAGE
#
# in your rack.rb file
# require this file and then:
#
# couch = CouchRest.new
# LOG_DB = couch.database!('couchrest-logger')
# use CouchRest::Logger, LOG_DB
#
# Note:
# to require just this middleware, if you have the gem installed do:
# require 'couchrest/middlewares/logger'
#
# For log processing examples, see examples at the bottom of this file
module CouchRest
class Logger
def self.log
Thread.current["couchrest.logger"] ||= {:queries => []}
end
def initialize(app, db=nil)
@app = app
@db = db
end
def self.record(log_info)
log[:queries] << log_info
end
def log
Thread.current["couchrest.logger"] ||= {:queries => []}
end
def reset_log
Thread.current["couchrest.logger"] = nil
end
def call(env)
reset_log
log['started_at'] = Time.now
log['env'] = env
log['url'] = 'http://' + env['HTTP_HOST'] + env['REQUEST_URI']
response = @app.call(env)
log['ended_at'] = Time.now
log['duration'] = log['ended_at'] - log['started_at']
# let's report the log in a different thread so we don't slow down the app
@db ? Thread.new(@db, log){|db, rlog| db.save_doc(rlog);} : p(log.inspect)
response
end
end
end
# inject our logger into CouchRest HTTP abstraction layer
module HttpAbstraction
def self.get(uri, headers=nil)
start_query = Time.now
log = {:method => :get, :uri => uri, :headers => headers}
response = super(uri, headers=nil)
end_query = Time.now
log[:duration] = (end_query - start_query)
CouchRest::Logger.record(log)
response
end
def self.post(uri, payload, headers=nil)
start_query = Time.now
log = {:method => :post, :uri => uri, :payload => (payload ? (JSON.load(payload) rescue 'parsing error') : nil), :headers => headers}
response = super(uri, payload, headers=nil)
end_query = Time.now
log[:duration] = (end_query - start_query)
CouchRest::Logger.record(log)
response
end
def self.put(uri, payload, headers=nil)
start_query = Time.now
log = {:method => :put, :uri => uri, :payload => (payload ? (JSON.load(payload) rescue 'parsing error') : nil), :headers => headers}
response = super(uri, payload, headers=nil)
end_query = Time.now
log[:duration] = (end_query - start_query)
CouchRest::Logger.record(log)
response
end
def self.delete(uri, headers=nil)
start_query = Time.now
log = {:method => :delete, :uri => uri, :headers => headers}
response = super(uri, headers=nil)
end_query = Time.now
log[:duration] = (end_query - start_query)
CouchRest::Logger.record(log)
response
end
end
# Advanced usage example
#
#
# # DB VIEWS
# by_url = {
# :map =>
# "function(doc) {
# if(doc['url']){ emit(doc['url'], 1) };
# }",
# :reduce =>
# 'function (key, values, rereduce) {
# return(sum(values));
# };'
# }
# req_duration = {
# :map =>
# "function(doc) {
# if(doc['duration']){ emit(doc['url'], doc['duration']) };
# }",
# :reduce =>
# 'function (key, values, rereduce) {
# return(sum(values)/values.length);
# };'
# }
#
# query_duration = {
# :map =>
# "function(doc) {
# if(doc['queries']){
# doc.queries.forEach(function(query){
# if(query['duration'] && query['method']){
# emit(query['method'], query['duration'])
# }
# });
# };
# }" ,
# :reduce =>
# 'function (key, values, rereduce) {
# return(sum(values)/values.length);
# };'
# }
#
# action_queries = {
# :map =>
# "function(doc) {
# if(doc['queries']){
# emit(doc['url'], doc['queries'].length)
# };
# }",
# :reduce =>
# 'function (key, values, rereduce) {
# return(sum(values)/values.length);
# };'
# }
#
# action_time_spent_in_db = {
# :map =>
# "function(doc) {
# if(doc['queries']){
# var totalDuration = 0;
# doc.queries.forEach(function(query){
# totalDuration += query['duration']
# })
# emit(doc['url'], totalDuration)
# };
# }",
# :reduce =>
# 'function (key, values, rereduce) {
# return(sum(values)/values.length);
# };'
# }
#
# show_queries = %Q~function(doc, req) {
# var body = ""
# body += "<h1>" + doc['url'] + "</h1>"
# body += "<h2>Request duration in seconds: " + doc['duration'] + "</h2>"
# body += "<h3>" + doc['queries'].length + " queries</h3><ul>"
# if (doc.queries){
# doc.queries.forEach(function(query){
# body += "<li>"+ query['uri'] +"</li>"
# });
# };
# body += "</ul>"
# if(doc){ return { body: body} }
# }~
#
#
# couch = CouchRest.new
# LOG_DB = couch.database!('couchrest-logger')
# design_doc = LOG_DB.get("_design/stats") rescue nil
# LOG_DB.delete_doc design_doc rescue nil
# LOG_DB.save_doc({
# "_id" => "_design/stats",
# :views => {
# :by_url => by_url,
# :request_duration => req_duration,
# :query_duration => query_duration,
# :action_queries => action_queries,
# :action_time_spent_in_db => action_time_spent_in_db
# },
# :shows => {
# :queries => show_queries
# }
# })
#
# module CouchRest
# class Logger
#
# def self.roundup(value)
# begin
# value = Float(value)
# (value * 100).round.to_f / 100
# rescue
# value
# end
# end
#
# # Usage example:
# # CouchRest::Logger.average_request_duration(LOG_DB)['rows'].first['value']
# def self.average_request_duration(db)
# raw = db.view('stats/request_duration', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.average_query_duration(db)
# raw = db.view('stats/query_duration', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.average_get_query_duration(db)
# raw = db.view('stats/query_duration', :key => 'get', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.average_post_query_duration(db)
# raw = db.view('stats/query_duration', :key => 'post', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.average_queries_per_action(db)
# raw = db.view('stats/action_queries', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.average_db_time_per_action(db)
# raw = db.view('stats/action_time_spent_in_db', :reduce => true)
# (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
# end
#
# def self.stats(db)
# Thread.new(db){|db|
# puts "=== STATS ===\n"
# puts "average request duration: #{average_request_duration(db)}\n"
# puts "average query duration: #{average_query_duration(db)}\n"
# puts "average queries per action : #{average_queries_per_action(db)}\n"
# puts "average time spent in DB (per action): #{average_db_time_per_action(db)}\n"
# puts "===============\n"
# }
# end
#
# end
# end

View file

@ -19,9 +19,9 @@ module CouchRest
end end
# deletes an attachment directly from couchdb # deletes an attachment directly from couchdb
def delete_attachment(name) def delete_attachment(name, force=false)
raise ArgumentError, "doc.database required to delete_attachment" unless database raise ArgumentError, "doc.database required to delete_attachment" unless database
result = database.delete_attachment(self, name) result = database.delete_attachment(self, name, force)
self['_rev'] = result['rev'] self['_rev'] = result['rev']
result['ok'] result['ok']
end end

View file

@ -59,6 +59,10 @@ module CouchRest
@klass.all({:database => @database}.merge(opts), &block) @klass.all({:database => @database}.merge(opts), &block)
end end
def count(opts = {}, &block)
@klass.all({:database => @database, :raw => true, :limit => 0}.merge(opts), &block)['total_rows']
end
def first(opts = {}) def first(opts = {})
@klass.first({:database => @database}.merge(opts)) @klass.first({:database => @database}.merge(opts))
end end
@ -100,7 +104,7 @@ module CouchRest
end end
def refresh_design_doc def refresh_design_doc
@klass.refresh_design_doc @klass.refresh_design_doc_on(@database)
end end
def save_design_doc def save_design_doc

View file

@ -1,5 +1,14 @@
module CouchRest module CouchRest
module Mixins module Mixins
module PaginatedResults
def amount_pages
@amount_pages ||= 0
end
def amount_pages=(value)
@amount_pages = value
end
end
module Collection module Collection
def self.included(base) def self.included(base)
@ -115,7 +124,10 @@ module CouchRest
results = @database.view(@view_name, pagination_options(page, per_page)) results = @database.view(@view_name, pagination_options(page, per_page))
@amount_pages ||= (results['total_rows'].to_f / per_page.to_f).ceil @amount_pages ||= (results['total_rows'].to_f / per_page.to_f).ceil
remember_where_we_left_off(results, page) remember_where_we_left_off(results, page)
convert_to_container_array(results) results = convert_to_container_array(results)
results.extend(PaginatedResults)
results.amount_pages = @amount_pages
results
end end
# See Collection.paginated_each # See Collection.paginated_each
@ -181,7 +193,7 @@ module CouchRest
@target.inspect @target.inspect
end end
def convert_to_container_array(results) def convert_to_container_array(results)
if @container_class.nil? if @container_class.nil?
results results
else else

View file

@ -35,7 +35,7 @@ module CouchRest
'all' => { 'all' => {
'map' => "function(doc) { 'map' => "function(doc) {
if (doc['couchrest-type'] == '#{self.to_s}') { if (doc['couchrest-type'] == '#{self.to_s}') {
emit(null,1); emit(doc['_id'],1);
} }
}" }"
} }
@ -48,6 +48,11 @@ module CouchRest
save_design_doc save_design_doc
end end
def refresh_design_doc_on(db)
reset_design_doc
save_design_doc_on(db)
end
# Save the design doc onto the default database, and update the # Save the design doc onto the default database, and update the
# design_doc attribute # design_doc attribute
def save_design_doc def save_design_doc

View file

@ -12,7 +12,7 @@ module CouchRest
# name of the current class. Take the standard set of # name of the current class. Take the standard set of
# CouchRest::Database#view options. # CouchRest::Database#view options.
def all(opts = {}, &block) def all(opts = {}, &block)
view(:all, {:reduce => false}.merge(opts), &block) view(:all, opts, &block)
end end
# Returns the number of documents that have the "couchrest-type" field # Returns the number of documents that have the "couchrest-type" field
@ -39,7 +39,38 @@ module CouchRest
end end
# Load a document from the database by id # Load a document from the database by id
# No exceptions will be raised if the document isn't found
#
# ==== Returns
# Object:: if the document was found
# or
# Nil::
#
# === Parameters
# id<String, Integer>:: Document ID
# db<Database>:: optional option to pass a custom database to use
def get(id, db = database) def get(id, db = database)
begin
doc = db.get id
rescue
nil
else
new(doc)
end
end
# Load a document from the database by id
# An exception will be raised if the document isn't found
#
# ==== Returns
# Object:: if the document was found
# or
# Exception
#
# === Parameters
# id<String, Integer>:: Document ID
# db<Database>:: optional option to pass a custom database to use
def get!(id, db = database)
doc = db.get id doc = db.get id
new(doc) new(doc)
end end

View file

@ -65,9 +65,8 @@ module CouchRest
key = self.has_key?(property.name) ? property.name : property.name.to_sym key = self.has_key?(property.name) ? property.name : property.name.to_sym
# Don't cast the property unless it has a value # Don't cast the property unless it has a value
return unless self[key] return unless self[key]
target = property.type if property.type.is_a?(Array)
if target.is_a?(Array) klass = ::CouchRest.constantize(property.type[0])
klass = ::CouchRest.constantize(target[0])
arr = self[key].dup.collect do |value| arr = self[key].dup.collect do |value|
unless value.instance_of?(klass) unless value.instance_of?(klass)
value = convert_property_value(property, klass, value) value = convert_property_value(property, klass, value)
@ -78,7 +77,12 @@ module CouchRest
self[key] = klass != String ? CastedArray.new(arr) : arr self[key] = klass != String ? CastedArray.new(arr) : arr
self[key].casted_by = self if self[key].respond_to?(:casted_by) self[key].casted_by = self if self[key].respond_to?(:casted_by)
else else
klass = ::CouchRest.constantize(target) if property.type == 'boolean'
klass = TrueClass
else
klass = ::CouchRest.constantize(property.type)
end
unless self[key].instance_of?(klass) unless self[key].instance_of?(klass)
self[key] = convert_property_value(property, klass, self[property.name]) self[key] = convert_property_value(property, klass, self[property.name])
end end
@ -93,12 +97,15 @@ module CouchRest
end end
def convert_property_value(property, klass, value) def convert_property_value(property, klass, value)
if ((property.init_method == 'new') && klass.to_s == 'Time') if ((property.init_method == 'new') && klass == Time)
# Using custom time parsing method because Ruby's default method is toooo slow # Using custom time parsing method because Ruby's default method is toooo slow
value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
# Float instances don't get initialized with #new # Float instances don't get initialized with #new
elsif ((property.init_method == 'new') && klass.to_s == 'Float') elsif ((property.init_method == 'new') && klass == Float)
cast_float(value) cast_float(value)
# 'boolean' type is simply used to generate a property? accessor method
elsif ((property.init_method == 'new') && klass == TrueClass)
value
else else
klass.send(property.init_method, value.dup) klass.send(property.init_method, value.dup)
end end
@ -150,6 +157,18 @@ module CouchRest
end end
EOS EOS
if property.type == 'boolean'
class_eval <<-EOS, __FILE__, __LINE__
def #{property.name}?
if self['#{property.name}'].nil? || self['#{property.name}'] == false || self['#{property.name}'].to_s.downcase == 'false'
false
else
true
end
end
EOS
end
if property.alias if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym} alias #{property.alias.to_sym} #{property.name.to_sym}

View file

@ -95,11 +95,11 @@ module CouchRest
# Dispatches to any named view. # Dispatches to any named view.
def view(name, query={}, &block) def view(name, query={}, &block)
unless design_doc_fresh db = query.delete(:database) || database
refresh_design_doc unless design_doc_fresh
refresh_design_doc_on(db)
end end
query[:raw] = true if query[:reduce] query[:raw] = true if query[:reduce]
db = query.delete(:database) || database
raw = query.delete(:raw) raw = query.delete(:raw)
fetch_view_with_docs(db, name, query, raw, &block) fetch_view_with_docs(db, name, query, raw, &block)
end end

View file

@ -1,11 +1,12 @@
require File.join(File.dirname(__FILE__), '..', 'mixins', 'properties') require File.expand_path('../../mixins/properties', __FILE__)
module CouchRest module CouchRest
module CastedModel module CastedModel
def self.included(base) def self.included(base)
base.send(:include, CouchRest::Callbacks) base.send(:include, ::CouchRest::Callbacks)
base.send(:include, CouchRest::Mixins::Properties) base.send(:include, ::CouchRest::Mixins::Properties)
base.send(:attr_accessor, :casted_by) base.send(:attr_accessor, :casted_by)
base.send(:attr_accessor, :document_saved) base.send(:attr_accessor, :document_saved)
end end

View file

@ -104,6 +104,13 @@ module CouchRest
entries.empty? entries.empty?
end end
# Return size of errors hash
#
# Allows us to play nicely with Rails' helpers
def count
errors.size
end
def method_missing(meth, *args, &block) def method_missing(meth, *args, &block)
errors.send(meth, *args, &block) errors.send(meth, *args, &block)
end end

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest do describe CouchRest do

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest::Database do describe CouchRest::Database do
before(:each) do before(:each) do
@ -263,7 +263,11 @@ describe CouchRest::Database do
r['ok'].should == true r['ok'].should == true
doc = @db.get("attach-this") doc = @db.get("attach-this")
attachment = @db.fetch_attachment(doc,"couchdb.png") attachment = @db.fetch_attachment(doc,"couchdb.png")
attachment.should == image if attachment.respond_to?(:net_http_res)
attachment.net_http_res.body.should == image
else
attachment.should == image
end
end end
end end
@ -368,8 +372,18 @@ describe CouchRest::Database do
end end
it "should delete the attachment" do it "should delete the attachment" do
lambda { @db.fetch_attachment(@doc,'test.html') }.should_not raise_error lambda { @db.fetch_attachment(@doc,'test.html') }.should_not raise_error
@db.delete_attachment(@doc, "test.html") @db.delete_attachment(@doc, "test.html")
lambda { @db.fetch_attachment(@doc,'test.html') }.should raise_error(RestClient::ResourceNotFound) @doc = @db.get('mydocwithattachment') # avoid getting a 409
lambda{ @db.fetch_attachment(@doc,'test.html')}.should raise_error
end
it "should force a delete even if we get a 409" do
@doc['new_attribute'] = 'something new'
@db.put_attachment(@doc, 'test', File.open(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'attachments', 'test.html')).read)
# at this point the revision number changed, if we try to save doc one more time
# we would get a 409.
lambda{ @db.save_doc(@doc) }.should raise_error
lambda{ @db.delete_attachment(@doc, "test", true) }.should_not raise_error
end end
end end

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest::Design do describe CouchRest::Design do

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
class Video < CouchRest::Document; end class Video < CouchRest::Document; end

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest::Server do describe CouchRest::Server do

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest::Pager do describe CouchRest::Pager do
before(:all) do before(:all) do

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
describe CouchRest::Streamer do describe CouchRest::Streamer do
before(:all) do before(:all) do

View file

@ -1,4 +1,4 @@
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.expand_path('../../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
class Car < CouchRest::ExtendedDocument class Car < CouchRest::ExtendedDocument

View file

@ -1,6 +1,6 @@
# encoding: utf-8 # encoding: utf-8
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.expand_path('../../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'person')

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path('../../../spec_helper', __FILE__)
describe "ExtendedDocument attachments" do describe "ExtendedDocument attachments" do

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'cat')
@ -270,6 +270,15 @@ describe "ExtendedDocument" do
foundart = Article.get @art.id foundart = Article.get @art.id
foundart.title.should == "All About Getting" foundart.title.should == "All About Getting"
end end
it "should return nil if `get` is used and the document doesn't exist" do
foundart = Article.get 'matt aimonetti'
foundart.should be_nil
end
it "should raise an error if `get!` is used and the document doesn't exist" do
lambda{foundart = Article.get!('matt aimonetti')}.should raise_error
end
end end
describe "getting a model with a subobjects array" do describe "getting a model with a subobjects array" do
@ -561,7 +570,7 @@ describe "ExtendedDocument" do
end end
it "should make it go away" do it "should make it go away" do
@dobj.destroy @dobj.destroy
lambda{Basic.get(@dobj.id)}.should raise_error lambda{Basic.get!(@dobj.id)}.should raise_error
end end
end end

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/../../spec_helper' require File.expand_path("../../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')
@ -168,8 +168,11 @@ describe "ExtendedDocument views" do
end end
things[0]["doc"]["title"].should =='aaa' things[0]["doc"]["title"].should =='aaa'
end end
it "should barf on get if no database given" do it "should return nil on get if no database given" do
lambda{Unattached.get("aaa")}.should raise_error Unattached.get("aaa").should be_nil
end
it "should barf on get! if no database given" do
lambda{Unattached.get!("aaa")}.should raise_error
end end
it "should get from specific database" do it "should get from specific database" do
u = Unattached.get(@first_id, @db) u = Unattached.get(@first_id, @db)
@ -200,7 +203,7 @@ describe "ExtendedDocument views" do
before(:all) do before(:all) do
reset_test_db! reset_test_db!
# setup the class default doc to save the design doc # setup the class default doc to save the design doc
Unattached.use_database DB Unattached.use_database nil # just to be sure it is really unattached
@us = Unattached.on(DB) @us = Unattached.on(DB)
%w{aaa bbb ddd eee}.each do |title| %w{aaa bbb ddd eee}.each do |title|
u = @us.new(:title => title) u = @us.new(:title => title)
@ -212,6 +215,9 @@ describe "ExtendedDocument views" do
rs = @us.all rs = @us.all
rs.length.should == 4 rs.length.should == 4
end end
it "should count" do
@us.count.should == 4
end
it "should make the design doc upon first query" do it "should make the design doc upon first query" do
@us.by_title @us.by_title
doc = @us.design_doc doc = @us.design_doc
@ -348,7 +354,8 @@ describe "ExtendedDocument views" do
a = Article.new(:title => title, :date => Date.today) a = Article.new(:title => title, :date => Date.today)
a.save a.save
end end
end end
require 'date'
it "should return a proxy that looks like an array of 7 Article objects" do it "should return a proxy that looks like an array of 7 Article objects" do
articles = Article.by_date :key => Date.today articles = Article.by_date :key => Date.today
articles.class.should == Array articles.class.should == Array
@ -368,8 +375,7 @@ describe "ExtendedDocument views" do
end end
it "should have the amount of paginated pages" do it "should have the amount of paginated pages" do
articles = Article.by_date :key => Date.today articles = Article.by_date :key => Date.today
articles.paginate(:per_page => 3) articles.paginate(:per_page => 3).amount_pages.should == 3
articles.amount_pages.should == 3
end end
it "should provide a class method to access the collection directly" do it "should provide a class method to access the collection directly" do
articles = Article.collection_proxy_for('Article', 'by_date', :descending => true, articles = Article.collection_proxy_for('Article', 'by_date', :descending => true,

View file

@ -1,4 +1,4 @@
require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') require File.expand_path('../../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice') require File.join(FIXTURE_PATH, 'more', 'invoice')
@ -164,6 +164,33 @@ describe "ExtendedDocument properties" do
end end
end end
describe "casting to a boolean value" do
class RootBeerFloat < CouchRest::ExtendedDocument
use_database DB
property :tasty, :cast_as => :boolean
end
it "should add an accessor with a '?' for boolean attributes that returns true or false" do
RootBeerFloat.new(:tasty => true).tasty?.should == true
RootBeerFloat.new(:tasty => 'you bet').tasty?.should == true
RootBeerFloat.new(:tasty => 123).tasty?.should == true
RootBeerFloat.new(:tasty => false).tasty?.should == false
RootBeerFloat.new(:tasty => 'false').tasty?.should == false
RootBeerFloat.new(:tasty => 'FaLsE').tasty?.should == false
RootBeerFloat.new(:tasty => nil).tasty?.should == false
end
it "should return the real value when the default accessor is used" do
RootBeerFloat.new(:tasty => true).tasty.should == true
RootBeerFloat.new(:tasty => 'you bet').tasty.should == 'you bet'
RootBeerFloat.new(:tasty => 123).tasty.should == 123
RootBeerFloat.new(:tasty => 'false').tasty.should == 'false'
RootBeerFloat.new(:tasty => false).tasty.should == false
RootBeerFloat.new(:tasty => nil).tasty.should == nil
end
end
end end
end end