Merge commit 'mattetti/master'

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

View file

@ -0,0 +1,35 @@
module RestClientAdapter
module API
def proxy=(url)
RestClient.proxy = url
end
def proxy
RestClient.proxy
end
def get(uri, headers={})
RestClient.get(uri, headers)
end
def post(uri, payload, headers={})
RestClient.post(uri, payload, headers)
end
def put(uri, payload, headers={})
RestClient.put(uri, payload, headers)
end
def delete(uri, headers={})
RestClient.delete(uri, headers)
end
def copy(uri, headers)
RestClient::Request.execute( :method => :copy,
:url => uri,
:headers => headers)
end
end
end

View file

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

View file

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

View file

@ -0,0 +1,48 @@
require 'couchrest/core/adapters/restclient'
# Abstraction layet for HTTP communications.
#
# By defining a basic API that CouchRest is relying on,
# it allows for easy experimentations and implementations of various libraries.
#
# Most of the API is based on the RestClient API that was used in the early version of CouchRest.
#
module HttpAbstraction
# here is the list of exception expected by CouchRest
# please convert the underlying errors in this set of known
# exceptions.
class ResourceNotFound < StandardError; end
class RequestFailed < StandardError; end
class RequestTimeout < StandardError; end
class ServerBrokeConnection < StandardError; end
class Conflict < StandardError; end
# # Here is the API you need to implement if you want to write a new adapter
# # See adapters/restclient.rb for more information.
#
# def self.proxy=(url)
# end
#
# def self.proxy
# end
#
# def self.get(uri, headers=nil)
# end
#
# def self.post(uri, payload, headers=nil)
# end
#
# def self.put(uri, payload, headers=nil)
# end
#
# def self.delete(uri, headers=nil)
# end
#
# def self.copy(uri, headers)
# end
end
HttpAbstraction.extend(RestClientAdapter::API)

View file

@ -0,0 +1,222 @@
module CouchRest
module Mixins
module Collection
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Creates a new class method, find_all_<collection_name>, that will
# execute the view specified with the design_doc and view_name
# parameters, along with the specified view_options. This method will
# return the results of the view as an Array of objects which are
# instances of the class.
#
# This method is handy for objects that do not use the view_by method
# to declare their views.
def provides_collection(collection_name, design_doc, view_name, view_options)
class_eval <<-END, __FILE__, __LINE__ + 1
def self.find_all_#{collection_name}(options = {})
view_options = #{view_options.inspect} || {}
CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
end
END
end
# Fetch a group of objects from CouchDB. Options can include:
# :page - Specifies the page to load (starting at 1)
# :per_page - Specifies the number of objects to load per page
#
# Defaults are used if these options are not specified.
def paginate(options)
proxy = create_collection_proxy(options)
proxy.paginate(options)
end
# Iterate over the objects in a collection, fetching them from CouchDB
# in groups. Options can include:
# :page - Specifies the page to load
# :per_page - Specifies the number of objects to load per page
#
# Defaults are used if these options are not specified.
def paginated_each(options, &block)
proxy = create_collection_proxy(options)
proxy.paginated_each(options, &block)
end
# Create a CollectionProxy for the specified view and options.
# CollectionProxy behaves just like an Array, but offers support for
# pagination.
def collection_proxy_for(design_doc, view_name, view_options = {})
options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
create_collection_proxy(options)
end
private
def create_collection_proxy(options)
design_doc, view_name, view_options = parse_view_options(options)
CollectionProxy.new(@database, design_doc, view_name, view_options, self)
end
def parse_view_options(options)
design_doc = options.delete(:design_doc)
raise ArgumentError, 'design_doc is required' if design_doc.nil?
view_name = options.delete(:view_name)
raise ArgumentError, 'view_name is required' if view_name.nil?
default_view_options = (design_doc.class == Design &&
design_doc['views'][view_name.to_s] &&
design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
view_options = default_view_options.merge(options)
[design_doc, view_name, view_options]
end
end
class CollectionProxy
alias_method :proxy_respond_to?, :respond_to?
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 30
# Create a new CollectionProxy to represent the specified view. If a
# container class is specified, the proxy will create an object of the
# given type for each row that comes back from the view. If no
# container class is specified, the raw results are returned.
#
# The CollectionProxy provides support for paginating over a collection
# via the paginate, and paginated_each methods.
def initialize(database, design_doc, view_name, view_options = {}, container_class = nil)
raise ArgumentError, "database is a required parameter" if database.nil?
@database = database
@container_class = container_class
strip_pagination_options(view_options)
@view_options = view_options
if design_doc.class == Design
@view_name = "#{design_doc.name}/#{view_name}"
else
@view_name = "#{design_doc}/#{view_name}"
end
end
# See Collection.paginate
def paginate(options = {})
page, per_page = parse_options(options)
results = @database.view(@view_name, pagination_options(page, per_page))
remember_where_we_left_off(results, page)
convert_to_container_array(results)
end
# See Collection.paginated_each
def paginated_each(options = {}, &block)
page, per_page = parse_options(options)
begin
collection = paginate({:page => page, :per_page => per_page})
collection.each(&block)
page += 1
end until collection.size < per_page
end
def respond_to?(*args)
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
# Explicitly proxy === because the instance method removal above
# doesn't catch it.
def ===(other)
load_target
other === @target
end
private
def method_missing(method, *args)
if load_target
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@target.send(method, *args)
end
end
end
def load_target
unless loaded?
results = @database.view(@view_name, @view_options)
@target = convert_to_container_array(results)
end
@loaded = true
@target
end
def loaded?
@loaded
end
def reload
reset
load_target
self unless @target.nil?
end
def reset
@loaded = false
@target = nil
end
def inspect
load_target
@target.inspect
end
def convert_to_container_array(results)
if @container_class.nil?
results
else
results['rows'].collect { |row| @container_class.new(row['doc']) } unless results['rows'].nil?
end
end
def pagination_options(page, per_page)
view_options = @view_options.clone
if @last_key && @last_docid && @last_page == page - 1
view_options.delete(:key)
options = { :startkey => @last_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
else
options = { :limit => per_page, :skip => per_page * (page - 1) }
end
view_options.merge(options)
end
def parse_options(options)
page = options.delete(:page) || DEFAULT_PAGE
per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
[page.to_i, per_page.to_i]
end
def strip_pagination_options(options)
parse_options(options)
end
def remember_where_we_left_off(results, page)
last_row = results['rows'].last
if last_row
@last_key = last_row['key']
@last_docid = last_row['id']
end
@last_page = page
end
end
end
end
end

View file

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

View file

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

View file

@ -5,3 +5,4 @@ require File.join(File.dirname(__FILE__), 'design_doc')
require File.join(File.dirname(__FILE__), 'validation')
require File.join(File.dirname(__FILE__), 'extended_attachments')
require File.join(File.dirname(__FILE__), 'class_proxy')
require File.join(File.dirname(__FILE__), 'collection')

View file

@ -1,6 +1,25 @@
require 'time'
require File.join(File.dirname(__FILE__), '..', 'more', 'property')
class Time
# returns a local time value much faster than Time.parse
def self.mktime_with_offset(string)
string =~ /(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([\+\-])(\d{2})/
# $1 = year
# $2 = month
# $3 = day
# $4 = hours
# $5 = minutes
# $6 = seconds
# $7 = time zone direction
# $8 = tz difference
# utc time with wrong TZ info:
time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
time + tz_difference + zone_offset(time.zone)
end
end
module CouchRest
module Mixins
module Properties
@ -65,6 +84,7 @@ module CouchRest
end
associate_casted_to_parent(self[property.name], assigned)
end
end
def associate_casted_to_parent(casted, assigned)
@ -73,8 +93,12 @@ module CouchRest
end
def convert_property_value(property, klass, value)
if ((property.init_method == 'new') && klass.to_s == 'Time')
value.is_a?(String) ? Time.parse(value.dup) : value
if ((property.init_method == 'new') && klass.to_s == 'Time')
# Using custom time parsing method because Ruby's default method is toooo slow
value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
# Float instances don't get initialized with #new
elsif ((property.init_method == 'new') && klass.to_s == 'Float')
cast_float(value)
else
klass.send(property.init_method, value.dup)
end
@ -87,6 +111,14 @@ module CouchRest
cast_property(property, true)
end
def cast_float(value)
begin
Float(value)
rescue
value
end
end
module ClassMethods
def property(name, options={})
@ -146,4 +178,4 @@ module CouchRest
end
end
end
end

View file

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

View file

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

View file

@ -13,10 +13,11 @@ module CouchRest
include CouchRest::Mixins::DesignDoc
include CouchRest::Mixins::ExtendedAttachments
include CouchRest::Mixins::ClassProxy
include CouchRest::Mixins::Collection
def self.subclasses
@subclasses ||= []
end
def self.subclasses
@subclasses ||= []
end
def self.inherited(subklass)
subklass.send(:include, CouchRest::Mixins::Properties)
@ -51,6 +52,26 @@ module CouchRest
end
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document
def self.create(options)
instance = new(options)
instance.create
instance
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document or raises an exception
def self.create!(options)
instance = new(options)
instance.create!
instance
end
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
# on the document whenever saving occurs. CouchRest uses a pretty
# decent time format by default. See Time#to_json

View file

@ -40,7 +40,7 @@ module CouchRest
value = target.send(field_name)
return true if @options[:allow_nil] && value.nil?
value = value.kind_of?(Float) ? value.to_s('F') : value.to_s
value = (defined?(BigDecimal) && value.kind_of?(BigDecimal)) ? value.to_s('F') : value.to_s
error_message = @options[:message]
precision = @options[:precision]