Adding support for proxying and more refinements to views
This commit is contained in:
parent
63bb1bb6bd
commit
a78e3b74d6
|
@ -109,7 +109,7 @@ module CouchRest
|
||||||
base = options[:proxy] || options[:class_name]
|
base = options[:proxy] || options[:class_name]
|
||||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def #{attrib}
|
def #{attrib}
|
||||||
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]})
|
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]})
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
@ -140,7 +140,7 @@ module CouchRest
|
||||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def #{attrib}(reload = false)
|
def #{attrib}(reload = false)
|
||||||
return @#{attrib} unless @#{attrib}.nil? or reload
|
return @#{attrib} unless @#{attrib}.nil? or reload
|
||||||
ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
|
ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)}
|
||||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
|
|
|
@ -12,6 +12,7 @@ module CouchRest
|
||||||
include CouchRest::Model::DesignDoc
|
include CouchRest::Model::DesignDoc
|
||||||
include CouchRest::Model::ExtendedAttachments
|
include CouchRest::Model::ExtendedAttachments
|
||||||
include CouchRest::Model::ClassProxy
|
include CouchRest::Model::ClassProxy
|
||||||
|
include CouchRest::Model::Proxyable
|
||||||
include CouchRest::Model::Collection
|
include CouchRest::Model::Collection
|
||||||
include CouchRest::Model::PropertyProtection
|
include CouchRest::Model::PropertyProtection
|
||||||
include CouchRest::Model::Associations
|
include CouchRest::Model::Associations
|
||||||
|
@ -46,9 +47,12 @@ module CouchRest
|
||||||
# Options supported:
|
# Options supported:
|
||||||
#
|
#
|
||||||
# * :directly_set_attributes: true when data comes directly from database
|
# * :directly_set_attributes: true when data comes directly from database
|
||||||
|
# * :database: provide an alternative database
|
||||||
#
|
#
|
||||||
def initialize(doc = {}, options = {})
|
def initialize(doc = {}, options = {})
|
||||||
doc = prepare_all_attributes(doc, options)
|
doc = prepare_all_attributes(doc, options)
|
||||||
|
# set the instances database, if provided
|
||||||
|
self.database = options[:database] unless options[:database].nil?
|
||||||
super(doc)
|
super(doc)
|
||||||
unless self['_id'] && self['_rev']
|
unless self['_id'] && self['_rev']
|
||||||
self[self.model_type_key] = self.class.to_s
|
self[self.model_type_key] = self.class.to_s
|
||||||
|
|
|
@ -222,7 +222,7 @@ module CouchRest
|
||||||
if @container_class.nil?
|
if @container_class.nil?
|
||||||
results
|
results
|
||||||
else
|
else
|
||||||
results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
|
results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,18 +5,19 @@ module CouchRest
|
||||||
#
|
#
|
||||||
# A proxy class that allows view queries to be created using
|
# A proxy class that allows view queries to be created using
|
||||||
# chained method calls. After each call a new instance of the method
|
# chained method calls. After each call a new instance of the method
|
||||||
# is created based on the original in a similar fashion to ruby's sequel
|
# is created based on the original in a similar fashion to ruby's Sequel
|
||||||
# library, or Rails 3's Arel.
|
# library, or Rails 3's Arel.
|
||||||
#
|
#
|
||||||
# CouchDB views have inherent limitations, so joins and filters as used in
|
# CouchDB views have inherent limitations, so joins and filters as used in
|
||||||
# a normal relational database are not possible. At least not yet!
|
# a normal relational database are not possible.
|
||||||
#
|
#
|
||||||
class View
|
class View
|
||||||
include Enumerable
|
include Enumerable
|
||||||
|
|
||||||
attr_accessor :model, :name, :query, :result
|
attr_accessor :model, :name, :query, :result
|
||||||
|
|
||||||
# Initialize a new View object. This method should not be called from outside CouchRest Model.
|
# Initialize a new View object. This method should not be called from
|
||||||
|
# outside CouchRest Model.
|
||||||
def initialize(parent, new_query = {}, name = nil)
|
def initialize(parent, new_query = {}, name = nil)
|
||||||
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
||||||
raise "Name must be provided for view to be initialized" if name.nil?
|
raise "Name must be provided for view to be initialized" if name.nil?
|
||||||
|
@ -25,14 +26,14 @@ module CouchRest
|
||||||
# Default options:
|
# Default options:
|
||||||
self.query = { :reduce => false }
|
self.query = { :reduce => false }
|
||||||
elsif parent.is_a?(self.class)
|
elsif parent.is_a?(self.class)
|
||||||
self.model = parent.model
|
self.model = (new_query.delete(:proxy) || parent.model)
|
||||||
self.name = parent.name
|
self.name = parent.name
|
||||||
self.query = parent.query.dup
|
self.query = parent.query.dup
|
||||||
else
|
else
|
||||||
raise "View cannot be initialized without a parent Model or View"
|
raise "View cannot be initialized without a parent Model or View"
|
||||||
end
|
end
|
||||||
query.update(new_query)
|
query.update(new_query)
|
||||||
super
|
super()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,6 +111,13 @@ module CouchRest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check to see if the array of documents is empty. This *will*
|
||||||
|
# perform the query and return all documents ready to use, if you don't
|
||||||
|
# want to load anything, use +#total_rows+ or +#count+ instead.
|
||||||
|
def empty?
|
||||||
|
all.empty?
|
||||||
|
end
|
||||||
|
|
||||||
# Run through each document provided by the +#all+ method.
|
# Run through each document provided by the +#all+ method.
|
||||||
# This is also used by the Enumerator mixin to provide all the standard
|
# This is also used by the Enumerator mixin to provide all the standard
|
||||||
# ruby collection directly on the view.
|
# ruby collection directly on the view.
|
||||||
|
@ -141,6 +149,18 @@ module CouchRest
|
||||||
rows.map{|r| r.value}
|
rows.map{|r| r.value}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Accept requests as if the view was an array. Used for backwards compatibity
|
||||||
|
# with older queries:
|
||||||
|
#
|
||||||
|
# Model.all(:raw => true, :limit => 0)['total_rows']
|
||||||
|
#
|
||||||
|
# In this example, the raw option will be ignored, and the total rows
|
||||||
|
# will still be accessible.
|
||||||
|
#
|
||||||
|
def [](value)
|
||||||
|
execute[value]
|
||||||
|
end
|
||||||
|
|
||||||
# No yet implemented. Eventually this will provide a raw hash
|
# No yet implemented. Eventually this will provide a raw hash
|
||||||
# of the information CouchDB holds about the view.
|
# of the information CouchDB holds about the view.
|
||||||
def info
|
def info
|
||||||
|
@ -150,16 +170,11 @@ module CouchRest
|
||||||
|
|
||||||
# == View Filter Methods
|
# == View Filter Methods
|
||||||
#
|
#
|
||||||
# View filters return an copy of the view instance with the query
|
# View filters return a copy of the view instance with the query
|
||||||
# modified appropriatly. Errors will be raised if the methods
|
# modified appropriatly. Errors will be raised if the methods
|
||||||
# are combined in an incorrect fashion.
|
# are combined in an incorrect fashion.
|
||||||
#
|
#
|
||||||
|
|
||||||
# Specify the database the view should use. If not defined,
|
|
||||||
# an attempt will be made to load its value from the model.
|
|
||||||
def database(value)
|
|
||||||
update_query(:database => value)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Find all entries in the index whose key matches the value provided.
|
# Find all entries in the index whose key matches the value provided.
|
||||||
#
|
#
|
||||||
|
@ -229,7 +244,8 @@ module CouchRest
|
||||||
update_query(:skip => value)
|
update_query(:skip => value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use the reduce function on the view. If none is available this method will fail.
|
# Use the reduce function on the view. If none is available this method
|
||||||
|
# will fail.
|
||||||
def reduce
|
def reduce
|
||||||
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
||||||
update_query(:reduce => true)
|
update_query(:reduce => true)
|
||||||
|
@ -257,6 +273,25 @@ module CouchRest
|
||||||
update_query.include_docs!
|
update_query.include_docs!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
### Special View Filter Methods
|
||||||
|
|
||||||
|
# Specify the database the view should use. If not defined,
|
||||||
|
# an attempt will be made to load its value from the model.
|
||||||
|
def database(value)
|
||||||
|
update_query(:database => value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set the view's proxy that will be used instead of the model
|
||||||
|
# for any future searches. As soon as this enters the
|
||||||
|
# new object's initializer it will be removed and replace
|
||||||
|
# the model object.
|
||||||
|
#
|
||||||
|
# See the Proxyable mixin for more details.
|
||||||
|
#
|
||||||
|
def proxy(value)
|
||||||
|
update_query(:proxy => value)
|
||||||
|
end
|
||||||
|
|
||||||
# Return any cached values to their nil state so that any queries
|
# Return any cached values to their nil state so that any queries
|
||||||
# requested later will have a fresh set of data.
|
# requested later will have a fresh set of data.
|
||||||
def reset!
|
def reset!
|
||||||
|
@ -288,20 +323,22 @@ module CouchRest
|
||||||
def can_reduce?
|
def can_reduce?
|
||||||
!design_doc['views'][name]['reduce'].blank?
|
!design_doc['views'][name]['reduce'].blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_database
|
||||||
|
query[:database] || model.database
|
||||||
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
return self.result if result
|
return self.result if result
|
||||||
db = query[:database] || model.database
|
raise "Database must be defined in model or view!" if use_database.nil?
|
||||||
raise "Database must be defined in model or view!" if db.nil?
|
|
||||||
retryable = true
|
retryable = true
|
||||||
# Remove the reduce value if its not needed
|
# Remove the reduce value if its not needed
|
||||||
query.delete(:reduce) unless can_reduce?
|
query.delete(:reduce) unless can_reduce?
|
||||||
begin
|
begin
|
||||||
self.result = model.design_doc.view_on(db, name, query)
|
self.result = model.design_doc.view_on(use_database, name, query)
|
||||||
rescue RestClient::ResourceNotFound => e
|
rescue RestClient::ResourceNotFound => e
|
||||||
if retryable
|
if retryable
|
||||||
model.save_design_doc(db)
|
model.save_design_doc(use_database)
|
||||||
retryable = false
|
retryable = false
|
||||||
retry
|
retry
|
||||||
else
|
else
|
||||||
|
@ -334,9 +371,10 @@ module CouchRest
|
||||||
def create(model, name, opts = {})
|
def create(model, name, opts = {})
|
||||||
|
|
||||||
unless opts[:map]
|
unless opts[:map]
|
||||||
if opts[:by].nil? && name =~ /^by_(.+)/
|
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
|
||||||
opts[:by] = $1.split(/_and_/)
|
opts[:by] = $1.split(/_and_/)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
||||||
|
|
||||||
opts[:guards] ||= []
|
opts[:guards] ||= []
|
||||||
|
@ -373,9 +411,9 @@ module CouchRest
|
||||||
# A special wrapper class that provides easy access to the key
|
# A special wrapper class that provides easy access to the key
|
||||||
# fields in a result row.
|
# fields in a result row.
|
||||||
class ViewRow < Hash
|
class ViewRow < Hash
|
||||||
attr_accessor :model
|
attr_reader :model
|
||||||
def initialize(hash, model)
|
def initialize(hash, model)
|
||||||
self.model = model
|
@model = model
|
||||||
replace(hash)
|
replace(hash)
|
||||||
end
|
end
|
||||||
def id
|
def id
|
||||||
|
@ -393,7 +431,7 @@ module CouchRest
|
||||||
# Send a request for the linked document either using the "id" field's
|
# Send a request for the linked document either using the "id" field's
|
||||||
# value, or the ["value"]["_id"] used for linked documents.
|
# value, or the ["value"]["_id"] used for linked documents.
|
||||||
def doc
|
def doc
|
||||||
return model.create_from_database(self['doc']) if self['doc']
|
return model.build_from_database(self['doc']) if self['doc']
|
||||||
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
||||||
model.get(doc_id)
|
model.get(doc_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -88,7 +88,7 @@ module CouchRest
|
||||||
def get!(id, db = database)
|
def get!(id, db = database)
|
||||||
raise "Missing or empty document ID" if id.to_s.empty?
|
raise "Missing or empty document ID" if id.to_s.empty?
|
||||||
doc = db.get id
|
doc = db.get id
|
||||||
create_from_database(doc)
|
build_from_database(doc)
|
||||||
end
|
end
|
||||||
alias :find! :get!
|
alias :find! :get!
|
||||||
|
|
||||||
|
|
|
@ -93,12 +93,12 @@ module CouchRest
|
||||||
|
|
||||||
# Creates a new instance, bypassing attribute protection
|
# Creates a new instance, bypassing attribute protection
|
||||||
#
|
#
|
||||||
#
|
|
||||||
# ==== Returns
|
# ==== Returns
|
||||||
# a document instance
|
# a document instance
|
||||||
def create_from_database(doc = {})
|
#
|
||||||
|
def build_from_database(doc = {})
|
||||||
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
|
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
|
||||||
base.new(doc, :directly_set_attributes => true)
|
base.new(doc, :directly_set_attributes => true)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Defines an instance and save it directly to the database
|
# Defines an instance and save it directly to the database
|
||||||
|
|
152
lib/couchrest/model/proxyable.rb
Normal file
152
lib/couchrest/model/proxyable.rb
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
module CouchRest
|
||||||
|
module Model
|
||||||
|
# :nodoc: Because I like inventing words
|
||||||
|
module Proxyable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attr_accessor :model_proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
|
||||||
|
# Define a collection that will use the base model for the database connection
|
||||||
|
# details.
|
||||||
|
def proxy_for(model_name, options = {})
|
||||||
|
db_method = options[:database_method] || "proxy_database"
|
||||||
|
options[:class_name] ||= model_name.to_s.singularize.camelize
|
||||||
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
|
def #{model_name}
|
||||||
|
unless respond_to?('#{db_method}')
|
||||||
|
raise "Missing ##{db_method} method for proxy"
|
||||||
|
end
|
||||||
|
@#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(#{options[:class_name]}, self, '#{model_name}', #{db_method})
|
||||||
|
end
|
||||||
|
EOS
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxied_by(model_name, options = {})
|
||||||
|
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name)
|
||||||
|
attr_accessor model_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ModelProxy
|
||||||
|
|
||||||
|
attr_reader :model, :owner, :owner_name, :database
|
||||||
|
|
||||||
|
def initialize(model, owner, owner_name, database)
|
||||||
|
@model = model
|
||||||
|
@owner = owner
|
||||||
|
@owner_name = owner_name
|
||||||
|
@database = database
|
||||||
|
end
|
||||||
|
|
||||||
|
# Base
|
||||||
|
|
||||||
|
def new(*args)
|
||||||
|
proxy_update(model.new(*args))
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_from_database(doc = {})
|
||||||
|
proxy_update(model.build_from_database(doc))
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_missing(m, *args, &block)
|
||||||
|
if has_view?(m)
|
||||||
|
if model.respond_to?(m)
|
||||||
|
return model.send(m, *args).proxy(self)
|
||||||
|
else
|
||||||
|
query = args.shift || {}
|
||||||
|
return view(m, query, *args, &block)
|
||||||
|
end
|
||||||
|
elsif m.to_s =~ /^find_(by_.+)/
|
||||||
|
view_name = $1
|
||||||
|
if has_view?(view_name)
|
||||||
|
return first_from_view(view_name, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# DocumentQueries
|
||||||
|
|
||||||
|
def all(opts = {}, &block)
|
||||||
|
proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
|
||||||
|
end
|
||||||
|
|
||||||
|
def count(opts = {})
|
||||||
|
@model.count({:database => @database}.merge(opts))
|
||||||
|
end
|
||||||
|
|
||||||
|
def first(opts = {})
|
||||||
|
proxy_update(@model.first({:database => @database}.merge(opts)))
|
||||||
|
end
|
||||||
|
|
||||||
|
def last(opts = {})
|
||||||
|
proxy_update(@model.last({:database => @database}.merge(opts)))
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(id)
|
||||||
|
proxy_update(@model.get(id, @database))
|
||||||
|
end
|
||||||
|
alias :find :get
|
||||||
|
|
||||||
|
# Views
|
||||||
|
|
||||||
|
def has_view?(view)
|
||||||
|
@model.has_view?(view)
|
||||||
|
end
|
||||||
|
|
||||||
|
def view_by(*args)
|
||||||
|
@model.view_by(*args)
|
||||||
|
end
|
||||||
|
|
||||||
|
def view(name, query={}, &block)
|
||||||
|
proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_from_view(name, *args)
|
||||||
|
# add to first hash available, or add to end
|
||||||
|
(args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
|
||||||
|
proxy_update(@model.first_from_view(name, *args))
|
||||||
|
end
|
||||||
|
|
||||||
|
# DesignDoc
|
||||||
|
|
||||||
|
def design_doc
|
||||||
|
@model.design_doc
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_design_doc
|
||||||
|
@model.refresh_design_doc(@database)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_design_doc
|
||||||
|
@model.save_design_doc(@database)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Update the document's proxy details, specifically, the fields that
|
||||||
|
# link back to the original document.
|
||||||
|
def proxy_update(doc)
|
||||||
|
if doc
|
||||||
|
doc.database = @database if doc.respond_to?(:database=)
|
||||||
|
doc.model_proxy = self if doc.respond_to?(:model_proxy=)
|
||||||
|
doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=")
|
||||||
|
end
|
||||||
|
doc
|
||||||
|
end
|
||||||
|
|
||||||
|
def proxy_update_all(docs)
|
||||||
|
docs.each do |doc|
|
||||||
|
proxy_update(doc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,19 +9,19 @@ module CouchRest
|
||||||
|
|
||||||
# Ensure we have a class available so we can check for a usable view
|
# Ensure we have a class available so we can check for a usable view
|
||||||
# or add one if necessary.
|
# or add one if necessary.
|
||||||
def setup(klass)
|
def setup(model)
|
||||||
@klass = klass
|
@model = model
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def validate_each(document, attribute, value)
|
def validate_each(document, attribute, value)
|
||||||
view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
|
view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
|
||||||
|
model = document.model_proxy || @model
|
||||||
# Determine the base of the search
|
# Determine the base of the search
|
||||||
base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
|
base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
|
||||||
|
|
||||||
if base.respond_to?(:has_view?) && !base.has_view?(view_name)
|
if base.respond_to?(:has_view?) && !base.has_view?(view_name)
|
||||||
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
|
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
|
||||||
@klass.view_by attribute
|
model.view_by attribute
|
||||||
end
|
end
|
||||||
|
|
||||||
docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
|
docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
|
||||||
|
@ -36,7 +36,6 @@ module CouchRest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -131,7 +131,7 @@ module CouchRest
|
||||||
collection_proxy_for(design_doc, name, opts.merge({:database => db, :include_docs => true}))
|
collection_proxy_for(design_doc, name, opts.merge({:database => db, :include_docs => true}))
|
||||||
else
|
else
|
||||||
view = fetch_view db, name, opts.merge({:include_docs => true}), &block
|
view = fetch_view db, name, opts.merge({:include_docs => true}), &block
|
||||||
view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
|
view['rows'].collect{|r|build_from_database(r['doc'])} if view['rows']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,7 @@ require "couchrest/model/views"
|
||||||
require "couchrest/model/design_doc"
|
require "couchrest/model/design_doc"
|
||||||
require "couchrest/model/extended_attachments"
|
require "couchrest/model/extended_attachments"
|
||||||
require "couchrest/model/class_proxy"
|
require "couchrest/model/class_proxy"
|
||||||
|
require "couchrest/model/proxyable"
|
||||||
require "couchrest/model/collection"
|
require "couchrest/model/collection"
|
||||||
require "couchrest/model/associations"
|
require "couchrest/model/associations"
|
||||||
require "couchrest/model/configuration"
|
require "couchrest/model/configuration"
|
||||||
|
|
|
@ -34,10 +34,16 @@ describe "Model Base" do
|
||||||
@obj.should be_new_record
|
@obj.should be_new_record
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not failed on a nil value in argument" do
|
it "should not fail with nil argument" do
|
||||||
@obj = Basic.new(nil)
|
@obj = Basic.new(nil)
|
||||||
@obj.should_not be_nil
|
@obj.should_not be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should allow the database to be set" do
|
||||||
|
@obj = Basic.new(nil, :database => 'database')
|
||||||
|
@obj.database.should eql('database')
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "ActiveModel compatability Basic" do
|
describe "ActiveModel compatability Basic" do
|
||||||
|
|
|
@ -194,6 +194,15 @@ describe "Design View" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#empty?" do
|
||||||
|
it "should check the #all method for any results" do
|
||||||
|
all = mock("All")
|
||||||
|
all.should_receive(:empty?).and_return('win')
|
||||||
|
@obj.should_receive(:all).and_return(all)
|
||||||
|
@obj.empty?.should eql('win')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#each" do
|
describe "#each" do
|
||||||
it "should call each method on all" do
|
it "should call each method on all" do
|
||||||
@obj.should_receive(:all).and_return([])
|
@obj.should_receive(:all).and_return([])
|
||||||
|
@ -242,6 +251,13 @@ describe "Design View" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#[]" do
|
||||||
|
it "should execute and provide requested field" do
|
||||||
|
@obj.should_receive(:execute).and_return({'total_rows' => 2})
|
||||||
|
@obj['total_rows'].should eql(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "#info" do
|
describe "#info" do
|
||||||
it "should raise error" do
|
it "should raise error" do
|
||||||
lambda { @obj.info }.should raise_error
|
lambda { @obj.info }.should raise_error
|
||||||
|
@ -509,10 +525,8 @@ describe "Design View" do
|
||||||
it "should retry once on a resource not found error" do
|
it "should retry once on a resource not found error" do
|
||||||
@obj.should_receive(:can_reduce?).and_return(true)
|
@obj.should_receive(:can_reduce?).and_return(true)
|
||||||
@obj.model.should_receive(:save_design_doc)
|
@obj.model.should_receive(:save_design_doc)
|
||||||
@design_doc.should_receive(:view_on).ordered
|
@design_doc.should_receive(:view_on).ordered.and_raise(RestClient::ResourceNotFound)
|
||||||
.and_raise(RestClient::ResourceNotFound)
|
@design_doc.should_receive(:view_on).ordered.and_return('foos')
|
||||||
@design_doc.should_receive(:view_on).ordered
|
|
||||||
.and_return('foos')
|
|
||||||
@obj.send(:execute)
|
@obj.send(:execute)
|
||||||
@obj.result.should eql('foos')
|
@obj.result.should eql('foos')
|
||||||
end
|
end
|
||||||
|
@ -520,8 +534,7 @@ describe "Design View" do
|
||||||
it "should retry twice and fail on a resource not found error" do
|
it "should retry twice and fail on a resource not found error" do
|
||||||
@obj.should_receive(:can_reduce?).and_return(true)
|
@obj.should_receive(:can_reduce?).and_return(true)
|
||||||
@obj.model.should_receive(:save_design_doc)
|
@obj.model.should_receive(:save_design_doc)
|
||||||
@design_doc.should_receive(:view_on).twice
|
@design_doc.should_receive(:view_on).twice.and_raise(RestClient::ResourceNotFound)
|
||||||
.and_raise(RestClient::ResourceNotFound)
|
|
||||||
lambda { @obj.send(:execute) }.should raise_error(RestClient::ResourceNotFound)
|
lambda { @obj.send(:execute) }.should raise_error(RestClient::ResourceNotFound)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -577,7 +590,7 @@ describe "Design View" do
|
||||||
hash = {'doc' => {'_id' => '12345', 'name' => 'sam'}}
|
hash = {'doc' => {'_id' => '12345', 'name' => 'sam'}}
|
||||||
obj = @klass.new(hash, DesignViewModel)
|
obj = @klass.new(hash, DesignViewModel)
|
||||||
doc = mock('DesignViewModel')
|
doc = mock('DesignViewModel')
|
||||||
obj.model.should_receive(:create_from_database).with(hash['doc']).and_return(doc)
|
obj.model.should_receive(:build_from_database).with(hash['doc']).and_return(doc)
|
||||||
obj.doc.should eql(doc)
|
obj.doc.should eql(doc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,17 @@ describe "Model Persistence" do
|
||||||
describe "creating a new document from database" do
|
describe "creating a new document from database" do
|
||||||
|
|
||||||
it "should instantialize" do
|
it "should instantialize" do
|
||||||
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'})
|
doc = Article.build_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'})
|
||||||
doc.class.should eql(Article)
|
doc.class.should eql(Article)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should instantialize of same class if no couchrest-type included from DB" do
|
it "should instantialize of same class if no couchrest-type included from DB" do
|
||||||
doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'})
|
doc = Article.build_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'})
|
||||||
doc.class.should eql(Article)
|
doc.class.should eql(Article)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should instantialize document of different type" do
|
it "should instantialize document of different type" do
|
||||||
doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'})
|
doc = Article.build_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'})
|
||||||
doc.class.should eql(WithTemplateAndUniqueID)
|
doc.class.should eql(WithTemplateAndUniqueID)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
301
spec/couchrest/proxyable_spec.rb
Normal file
301
spec/couchrest/proxyable_spec.rb
Normal file
|
@ -0,0 +1,301 @@
|
||||||
|
require File.expand_path("../../spec_helper", __FILE__)
|
||||||
|
|
||||||
|
require File.join(FIXTURE_PATH, 'more', 'cat')
|
||||||
|
|
||||||
|
class DummyProxyable < CouchRest::Model::Base
|
||||||
|
def proxy_database
|
||||||
|
'db' # Do not use this!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ProxyKitten < CouchRest::Model::Base
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Proxyable" do
|
||||||
|
|
||||||
|
it "should provide #model_proxy method" do
|
||||||
|
DummyProxyable.new.should respond_to(:model_proxy)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "class methods" do
|
||||||
|
|
||||||
|
describe ".proxy_for" do
|
||||||
|
|
||||||
|
it "should be provided" do
|
||||||
|
DummyProxyable.should respond_to(:proxy_for)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create a new method" do
|
||||||
|
DummyProxyable.stub!(:method_defined?).and_return(true)
|
||||||
|
DummyProxyable.proxy_for(:cats)
|
||||||
|
DummyProxyable.new.should respond_to(:cats)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "generated method" do
|
||||||
|
it "should call ModelProxy" do
|
||||||
|
DummyProxyable.proxy_for(:cats)
|
||||||
|
@obj = DummyProxyable.new
|
||||||
|
CouchRest::Model::Proxyable::ModelProxy.should_receive(:new).with(Cat, @obj, 'cats', 'db').and_return(true)
|
||||||
|
@obj.should_receive('proxy_database').and_return('db')
|
||||||
|
@obj.should_receive(:respond_to?).with('proxy_database').and_return(true)
|
||||||
|
@obj.cats
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should raise an error if the database method is missing" do
|
||||||
|
DummyProxyable.proxy_for(:cats)
|
||||||
|
@obj = DummyProxyable.new
|
||||||
|
@obj.should_receive(:respond_to?).with('proxy_database').and_return(false)
|
||||||
|
lambda { @obj.cats }.should raise_error(StandardError, "Missing #proxy_database method for proxy")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should raise an error if custom database method missing" do
|
||||||
|
DummyProxyable.proxy_for(:proxy_kittens, :database_method => "foobardom")
|
||||||
|
@obj = DummyProxyable.new
|
||||||
|
lambda { @obj.proxy_kittens }.should raise_error(StandardError, "Missing #foobardom method for proxy")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".proxied_by" do
|
||||||
|
it "should be provided" do
|
||||||
|
DummyProxyable.should respond_to(:proxied_by)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should add an attribute accessor" do
|
||||||
|
DummyProxyable.proxied_by(:foobar)
|
||||||
|
DummyProxyable.new.should respond_to(:foobar)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should raise an error if model name pre-defined" do
|
||||||
|
lambda { DummyProxyable.proxied_by(:object_id) }.should raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "ModelProxy" do
|
||||||
|
|
||||||
|
before :all do
|
||||||
|
@klass = CouchRest::Model::Proxyable::ModelProxy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should initialize and set variables" do
|
||||||
|
@obj = @klass.new(Cat, 'owner', 'owner_name', 'database')
|
||||||
|
@obj.model.should eql(Cat)
|
||||||
|
@obj.owner.should eql('owner')
|
||||||
|
@obj.owner_name.should eql('owner_name')
|
||||||
|
@obj.database.should eql('database')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "instance" do
|
||||||
|
|
||||||
|
before :each do
|
||||||
|
@obj = @klass.new(Cat, 'owner', 'owner_name', 'database')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy new call" do
|
||||||
|
Cat.should_receive(:new).and_return({})
|
||||||
|
@obj.should_receive(:proxy_update).and_return(true)
|
||||||
|
@obj.new
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy build_from_database" do
|
||||||
|
Cat.should_receive(:build_from_database).and_return({})
|
||||||
|
@obj.should_receive(:proxy_update).with({}).and_return(true)
|
||||||
|
@obj.build_from_database
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#method_missing" do
|
||||||
|
it "should return design view object" do
|
||||||
|
m = "by_some_property"
|
||||||
|
inst = mock('DesignView')
|
||||||
|
inst.stub!(:proxy).and_return(inst)
|
||||||
|
@obj.should_receive(:has_view?).with(m).and_return(true)
|
||||||
|
Cat.should_receive(:respond_to?).with(m).and_return(true)
|
||||||
|
Cat.should_receive(:send).with(m).and_return(inst)
|
||||||
|
@obj.method_missing(m).should eql(inst)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call view if necessary" do
|
||||||
|
m = "by_some_property"
|
||||||
|
@obj.should_receive(:has_view?).with(m).and_return(true)
|
||||||
|
Cat.should_receive(:respond_to?).with(m).and_return(false)
|
||||||
|
@obj.should_receive(:view).with(m, {}).and_return('view')
|
||||||
|
@obj.method_missing(m).should eql('view')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should provide wrapper for #first_from_view" do
|
||||||
|
m = "find_by_some_property"
|
||||||
|
view = "by_some_property"
|
||||||
|
@obj.should_receive(:has_view?).with(m).and_return(false)
|
||||||
|
@obj.should_receive(:has_view?).with(view).and_return(true)
|
||||||
|
@obj.should_receive(:first_from_view).with(view).and_return('view')
|
||||||
|
@obj.method_missing(m).should eql('view')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #all" do
|
||||||
|
Cat.should_receive(:all).with({:database => 'database'})
|
||||||
|
@obj.should_receive(:proxy_update_all)
|
||||||
|
@obj.all
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #count" do
|
||||||
|
Cat.should_receive(:all).with({:database => 'database', :raw => true, :limit => 0}).and_return({'total_rows' => 3})
|
||||||
|
@obj.count.should eql(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #first" do
|
||||||
|
Cat.should_receive(:first).with({:database => 'database'})
|
||||||
|
@obj.should_receive(:proxy_update)
|
||||||
|
@obj.first
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #last" do
|
||||||
|
Cat.should_receive(:last).with({:database => 'database'})
|
||||||
|
@obj.should_receive(:proxy_update)
|
||||||
|
@obj.last
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #get" do
|
||||||
|
Cat.should_receive(:get).with(32, 'database')
|
||||||
|
@obj.should_receive(:proxy_update)
|
||||||
|
@obj.get(32)
|
||||||
|
end
|
||||||
|
it "should proxy #find" do
|
||||||
|
Cat.should_receive(:get).with(32, 'database')
|
||||||
|
@obj.should_receive(:proxy_update)
|
||||||
|
@obj.find(32)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #has_view?" do
|
||||||
|
Cat.should_receive(:has_view?).with('view').and_return(false)
|
||||||
|
@obj.has_view?('view')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #view_by" do
|
||||||
|
Cat.should_receive(:view_by).with('name').and_return(false)
|
||||||
|
@obj.view_by('name')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #view" do
|
||||||
|
Cat.should_receive(:view).with('view', {:database => 'database'})
|
||||||
|
@obj.should_receive(:proxy_update_all)
|
||||||
|
@obj.view('view')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy #first_from_view" do
|
||||||
|
Cat.should_receive(:first_from_view).with('view', {:database => 'database'})
|
||||||
|
@obj.should_receive(:proxy_update)
|
||||||
|
@obj.first_from_view('view')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy design_doc" do
|
||||||
|
Cat.should_receive(:design_doc)
|
||||||
|
@obj.design_doc
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy refresh_design_doc" do
|
||||||
|
Cat.should_receive(:refresh_design_doc).with('database')
|
||||||
|
@obj.refresh_design_doc
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should proxy save_design_doc" do
|
||||||
|
Cat.should_receive(:save_design_doc).with('database')
|
||||||
|
@obj.save_design_doc
|
||||||
|
end
|
||||||
|
|
||||||
|
### Updating methods
|
||||||
|
|
||||||
|
describe "#proxy_update" do
|
||||||
|
it "should set returned doc fields" do
|
||||||
|
doc = mock(:Document)
|
||||||
|
doc.should_receive(:respond_to?).with(:database=).and_return(true)
|
||||||
|
doc.should_receive(:database=).with('database')
|
||||||
|
doc.should_receive(:respond_to?).with(:model_proxy=).and_return(true)
|
||||||
|
doc.should_receive(:model_proxy=).with(@obj)
|
||||||
|
doc.should_receive(:respond_to?).with('owner_name=').and_return(true)
|
||||||
|
doc.should_receive(:send).with('owner_name=', 'owner')
|
||||||
|
@obj.send(:proxy_update, doc).should eql(doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should not fail if some fields missing" do
|
||||||
|
doc = mock(:Document)
|
||||||
|
doc.should_receive(:respond_to?).with(:database=).and_return(true)
|
||||||
|
doc.should_receive(:database=).with('database')
|
||||||
|
doc.should_receive(:respond_to?).with(:model_proxy=).and_return(false)
|
||||||
|
doc.should_not_receive(:model_proxy=)
|
||||||
|
doc.should_receive(:respond_to?).with('owner_name=').and_return(false)
|
||||||
|
doc.should_not_receive(:owner_name=)
|
||||||
|
@obj.send(:proxy_update, doc).should eql(doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should pass nil straight through without errors" do
|
||||||
|
lambda { @obj.send(:proxy_update, nil).should eql(nil) }.should_not raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "#proxy_update_all should update array of docs" do
|
||||||
|
docs = [{}, {}]
|
||||||
|
@obj.should_receive(:proxy_update).twice.with({})
|
||||||
|
@obj.send(:proxy_update_all, docs)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "scenarios" do
|
||||||
|
|
||||||
|
before :all do
|
||||||
|
class ProxyableCompany < CouchRest::Model::Base
|
||||||
|
use_database DB
|
||||||
|
property :slug
|
||||||
|
proxy_for :proxyable_invoices
|
||||||
|
def proxy_database
|
||||||
|
@db ||= TEST_SERVER.database!(TESTDB + "-#{slug}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ProxyableInvoice < CouchRest::Model::Base
|
||||||
|
property :client
|
||||||
|
property :total
|
||||||
|
proxied_by :proxyable_company
|
||||||
|
validates_uniqueness_of :client
|
||||||
|
design do
|
||||||
|
view :by_total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@company = ProxyableCompany.create(:slug => 'samco')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create the new database" do
|
||||||
|
@company.proxyable_invoices.all.should be_empty
|
||||||
|
TEST_SERVER.databases.find{|db| db =~ /#{TESTDB}-samco/}.should_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow creation of new entries" do
|
||||||
|
inv = @company.proxyable_invoices.new(:client => "Lorena", :total => 35)
|
||||||
|
inv.save.should be_true
|
||||||
|
@company.proxyable_invoices.count.should eql(1)
|
||||||
|
@company.proxyable_invoices.first.client.should eql("Lorena")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should validate uniqueness" do
|
||||||
|
inv = @company.proxyable_invoices.new(:client => "Lorena", :total => 40)
|
||||||
|
inv.save.should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow design views" do
|
||||||
|
item = @company.proxyable_invoices.by_total.key(35).first
|
||||||
|
item.client.should eql('Lorena')
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in a new issue