Merge commit 'mattetti/master'
This commit is contained in:
commit
1e44302d1a
25 changed files with 695 additions and 135 deletions
222
lib/couchrest/mixins/collection.rb
Normal file
222
lib/couchrest/mixins/collection.rb
Normal 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
|
|
@ -37,9 +37,6 @@ module CouchRest
|
|||
if (doc['couchrest-type'] == '#{self.to_s}') {
|
||||
emit(null,1);
|
||||
}
|
||||
}",
|
||||
'reduce' => "function(keys, values) {
|
||||
return sum(values);
|
||||
}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue